diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index aedf7f4348..a3fe15b316 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -667,12 +667,7 @@ PLATFORM_HINTS = { "height live, width from the content's first measured span — lay content flush left with no centering wrappers " "or it measures full-bleed. Widgets talk back: data-hermes-send=\"prompt\" on any clickable element (or " "window.hermes.send(\"prompt\")) sends that prompt as a hidden user turn — answer it by updating the widget's " - "file, not with prose. Property/rental listings render as browsable cards: emit a ```listing fence " - "holding JSON — one object, or an array to compare several — with address (required), price, beds, " - "baths, size, note (why it is worth a look), facts[] (short specs), catches[] (risks to verify), " - "images[] (direct https photo URLs, in listing order — the first is the hero), and links[] " - "({label, url} detail pages, never a search-results URL). Use it for every property you present, " - "including follow-ups and re-rankings, so listings stay comparable." + "file, not with prose." ), "sms": ( "You are communicating via SMS. Keep responses concise and use plain text only — no markdown, no " diff --git a/evals/prompt_footprint/property_guidance.py b/evals/prompt_footprint/property_guidance.py new file mode 100644 index 0000000000..0f7b38af05 --- /dev/null +++ b/evals/prompt_footprint/property_guidance.py @@ -0,0 +1,76 @@ +"""Offline prompt A/B and real optional catalog -> skill_view probe. +Run with the repository venv; tiktoken must be available. No model API calls. +""" +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +from types import SimpleNamespace + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + + +def main(): + with tempfile.TemporaryDirectory(prefix="property-guidance-") as temporary: + os.environ["HERMES_HOME"] = temporary + os.environ["TERMINAL_CWD"] = temporary + os.chdir(temporary) + import tiktoken + from agent import prompt_builder + from agent.system_prompt import build_system_prompt + from tools.skills_hub_official import OptionalSkillSource + from tools.skills_tool import skill_view + + agent = SimpleNamespace( + load_soul_identity=False, skip_context_files=True, valid_tool_names=[], + _task_completion_guidance=False, _tool_use_enforcement=False, + _environment_probe=False, _kanban_worker_guidance="", _memory_store=None, + _memory_manager=None, model="", provider="", platform="desktop", + pass_session_id=False, session_id="", _emit_status=lambda *args: None, + ) + fixed_hint = prompt_builder.PLATFORM_HINTS["desktop"] + # The original clause is taken from the pinned base, never synthesized. + source = subprocess.check_output( + ["git", "show", f"{sys.argv[1]}:agent/prompt_builder.py"], cwd=ROOT, + text=True, stdin=subprocess.DEVNULL, + ) + import ast + tree = ast.parse(source) + mapping = next(n.value for n in tree.body if isinstance(n, ast.Assign) + and any(isinstance(t, ast.Name) and t.id == "PLATFORM_HINTS" for t in n.targets)) + base_hint = next(ast.literal_eval(value) for key, value in zip(mapping.keys, mapping.values) + if isinstance(key, ast.Constant) and key.value == "desktop") + prompt_builder.PLATFORM_HINTS["desktop"] = base_hint + before = build_system_prompt(agent) + prompt_builder.PLATFORM_HINTS["desktop"] = fixed_hint + after = build_system_prompt(agent) + assert before.replace(base_hint, fixed_hint) == after + result = {"base": sys.argv[1], "tokenizers": {}} + for name in ("cl100k_base", "o200k_base"): + enc = tiktoken.get_encoding(name) + counts = [len(enc.encode(s)) for s in (base_hint, fixed_hint, before, after)] + result["tokenizers"][name] = dict(zip( + ("hint_before", "hint_after", "prompt_before", "prompt_after"), counts)) + optional = OptionalSkillSource() + matches = [m for m in optional.list_local() if "property" in m.tags and "rental" in m.tags] + assert matches + bundle = optional.fetch(matches[0].identifier) + assert bundle + dest = Path(temporary) / "skills" / bundle.name + dest.mkdir(parents=True) + for name, data in bundle.files.items(): + (dest / name).write_bytes(data if isinstance(data, bytes) else data.encode("utf-8")) + loaded = json.loads(skill_view(bundle.name)) + assert loaded["success"], loaded + example = loaded["content"].split("```listing\n", 1)[1].split("```", 1)[0] + assert json.loads(example)["address"] + result.update(identifier=matches[0].identifier, skill_view_success=True, + non_property_prompt_byte_parity=True, example=json.loads(example)) + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/optional-skills/productivity/property-listings/SKILL.md b/optional-skills/productivity/property-listings/SKILL.md new file mode 100644 index 0000000000..42c45ec5b5 --- /dev/null +++ b/optional-skills/productivity/property-listings/SKILL.md @@ -0,0 +1,105 @@ +--- +name: property-listings +description: Present property and rental listings as desktop cards. +version: 0.1.0 +author: Teknium (teknium1), Hermes Agent +license: MIT +platforms: [linux, macos, windows] +metadata: + hermes: + tags: [property, rental, real-estate, listings, desktop, cards] + category: productivity + related_skills: [] +--- + +# Property Listings Skill + +Present researched properties as browsable cards in the Hermes desktop transcript. +This is a presentation recipe, not a listing search service or an investment valuation. + +## When to Use + +- Presenting property or rental search results, comparing a shortlist, or re-ranking properties. +- Following up on a property already shown: keep using cards so the shortlist stays comparable. +- Outside the desktop app, use ordinary Markdown with source links instead; other clients need not render listing fences. + +## Prerequisites + +- A Hermes desktop conversation for native cards; the backend may be local or remote. +- Property details supplied by the user or verified through `web_search`, `web_extract`, or the browser tools available in this session. +- No additional API keys or dependencies are required for card formatting. + +## How to Run + +Install this optional skill through the Skills catalog, or use `terminal`: + +```text +hermes skills install official/productivity/property-listings +``` + +Load it with `skill_view(name="property-listings")` when presenting listings. +Installing does not retrofit the running conversation's skill index; start a new +conversation for automatic discovery, or explicitly load the installed skill now. + +## Quick Reference + +Emit a fenced code block whose language is `listing` and whose body is valid JSON. +Use one object, an array of objects, or `{ "listings": [...] }` for a comparison. + +| Field | Shape and meaning | +|---|---| +| `address` | Required nonempty street address or property headline. | +| `price` | Formatted string including currency and rental period, if applicable. | +| `beds`, `baths` | Positive numeric counts; omit unknown values. | +| `size` | Formatted area including units. | +| `note` | Why this property is worth a look. | +| `facts` | Array of short verified specs or amenities. | +| `catches` | Array of risks or questions to verify before a tour. | +| `images` | Direct HTTPS photo URLs in listing order; the first is the hero. | +| `links` | Array of `{ "label": "Source", "url": "https://..." }` detail-page links, not search-result URLs. | + +## Procedure + +1. Gather the address, price, specs, photos and canonical detail URL. Distinguish + verified facts from unknowns; do not invent prices, amenities, or photo URLs. +2. Deduplicate portal mirrors of the same property into one card, retaining useful + source links. Keep source dates and availability caveats in the surrounding prose. +3. Emit the `listing` fence for every property presented, including follow-ups and + re-rankings. Keep facts short and put unresolved concerns in `catches`. +4. Check the JSON before sending. This fictional format example illustrates all fields; + replace its values and example URLs with verified listing data: + +```listing +{ + "address": "12 Example Lane", + "price": "$2,400/mo", + "beds": 3, + "baths": 2.5, + "size": "1,600 sqft", + "note": "Fits the requested space and budget.", + "facts": ["12-month lease", "Covered parking"], + "catches": ["Verify pet policy and total move-in fees"], + "images": ["https://example.com/property/front.jpg", "https://example.com/property/kitchen.jpg"], + "links": [{"label": "Listing details", "url": "https://example.com/property/12"}] +} +``` + +## Pitfalls + +- Cards are authored from gathered data, not fetched from a listing URL or embedded portal page. +- A sparse card needs only an address. Omit unknown fields rather than filling them with guesses. +- Use direct remote image URLs, not local paths, data URLs, or search-result pages. + Expired or blocked images disappear from the gallery; the text and links still matter. +- Keep a fence to at most 24 properties, 40 images per property, and 12 entries in + facts, catches and links. Text fields are truncated to 400 characters by the renderer. +- Malformed JSON or a card without identity falls back to a plain code block. + A valid card is not proof that the underlying listing is current or accurate. + +## Verification + +- Every presented property has an address and a verified source link; unknowns are explicit. +- Desktop displays the address, price, specs, facts, catches and links as a native card. +- Photos form a gallery; selecting a photo opens the lightbox. Three or more photos + use a hero-and-supporting-frames mosaic; additional photos remain browsable there. +- If the card fails to render, validate the fence language and JSON, then preserve a + readable Markdown fallback with the same facts and links. diff --git a/tests/skills/test_property_listings_skill.py b/tests/skills/test_property_listings_skill.py new file mode 100644 index 0000000000..76833c474e --- /dev/null +++ b/tests/skills/test_property_listings_skill.py @@ -0,0 +1,38 @@ +"""Property card recipes are optional, discoverable, and loadable on demand.""" +import json +from pathlib import Path + +from agent.prompt_builder import PLATFORM_HINTS +from tools.skills_hub_official import OptionalSkillSource + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_property_recipe_is_not_paid_for_by_unrelated_desktop_sessions(): + hint = PLATFORM_HINTS["desktop"] + assert "```listing" not in hint + assert "MEDIA:" in hint and "::preview" in hint + + +def test_optional_catalog_fetch_preserves_a_usable_property_recipe(tmp_path, monkeypatch): + source = OptionalSkillSource() + source._optional_dir = ROOT / "optional-skills" + matches = [m for m in source.list_local() if "property" in m.tags and "rental" in m.tags] + assert matches, "Property tasks must be discoverable in the optional catalog" + bundle = source.fetch(matches[0].identifier) + assert bundle is not None + from tools.skills_tool import skill_view + + monkeypatch.setenv("HERMES_HOME", str(tmp_path)) + destination = tmp_path / "skills" / bundle.name + destination.mkdir(parents=True) + for name, data in bundle.files.items(): + (destination / name).write_bytes(data if isinstance(data, bytes) else data.encode("utf-8")) + loaded = json.loads(skill_view(bundle.name)) + assert loaded["success"], loaded + content = loaded["content"] + example = content.split("```listing\n", 1)[1].split("```", 1)[0] + listing = json.loads(example) + assert listing["address"] and listing["links"] + assert {"price", "beds", "baths", "size", "note", "facts", "catches", "images"} <= listing.keys() diff --git a/website/docs/reference/optional-skills-catalog.md b/website/docs/reference/optional-skills-catalog.md index 2c6cce8fcd..ea74ac0976 100644 --- a/website/docs/reference/optional-skills-catalog.md +++ b/website/docs/reference/optional-skills-catalog.md @@ -207,6 +207,7 @@ hermes skills uninstall | [**decision-questionnaire**](/docs/user-guide/skills/optional/productivity/productivity-decision-questionnaire) | Turn an unanswerable decision into a questionnaire doc. | | [**here-now**](/docs/user-guide/skills/optional/productivity/productivity-here-now) | Publish sites to {slug}.here.now and store files in Drives. | | [**memento-flashcards**](/docs/user-guide/skills/optional/productivity/productivity-memento-flashcards) | Spaced-repetition flashcards: create, review, quiz, export. | +| [**property-listings**](/docs/user-guide/skills/optional/productivity/productivity-property-listings) | Present property and rental listings as desktop cards. | | [**shop**](/docs/user-guide/skills/optional/productivity/productivity-shop) | Shop catalog search, checkout, order tracking, returns. | | [**shopify**](/docs/user-guide/skills/optional/productivity/productivity-shopify) | Query Shopify Admin/Storefront GraphQL APIs via curl. | | [**siyuan**](/docs/user-guide/skills/optional/productivity/productivity-siyuan) | Query and edit a SiYuan knowledge base via its API. | diff --git a/website/docs/user-guide/skills/optional/productivity/productivity-property-listings.md b/website/docs/user-guide/skills/optional/productivity/productivity-property-listings.md new file mode 100644 index 0000000000..664697b200 --- /dev/null +++ b/website/docs/user-guide/skills/optional/productivity/productivity-property-listings.md @@ -0,0 +1,121 @@ +--- +title: "Property Listings — Present property and rental listings as desktop cards" +sidebar_label: "Property Listings" +description: "Present property and rental listings as desktop cards" +--- + +{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} + +# Property Listings + +Present property and rental listings as desktop cards. + +## Skill metadata + +| | | +|---|---| +| Source | Optional — install with `hermes skills install official/productivity/property-listings` | +| Path | `optional-skills/productivity/property-listings` | +| Version | `0.1.0` | +| Author | Teknium (teknium1), Hermes Agent | +| License | MIT | +| Platforms | linux, macos, windows | +| Tags | `property`, `rental`, `real-estate`, `listings`, `desktop`, `cards` | + +## Reference: full SKILL.md + +:::info +The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active. +::: + +# Property Listings Skill + +Present researched properties as browsable cards in the Hermes desktop transcript. +This is a presentation recipe, not a listing search service or an investment valuation. + +## When to Use + +- Presenting property or rental search results, comparing a shortlist, or re-ranking properties. +- Following up on a property already shown: keep using cards so the shortlist stays comparable. +- Outside the desktop app, use ordinary Markdown with source links instead; other clients need not render listing fences. + +## Prerequisites + +- A Hermes desktop conversation for native cards; the backend may be local or remote. +- Property details supplied by the user or verified through `web_search`, `web_extract`, or the browser tools available in this session. +- No additional API keys or dependencies are required for card formatting. + +## How to Run + +Install this optional skill through the Skills catalog, or use `terminal`: + +```text +hermes skills install official/productivity/property-listings +``` + +Load it with `skill_view(name="property-listings")` when presenting listings. +Installing does not retrofit the running conversation's skill index; start a new +conversation for automatic discovery, or explicitly load the installed skill now. + +## Quick Reference + +Emit a fenced code block whose language is `listing` and whose body is valid JSON. +Use one object, an array of objects, or `{ "listings": [...] }` for a comparison. + +| Field | Shape and meaning | +|---|---| +| `address` | Required nonempty street address or property headline. | +| `price` | Formatted string including currency and rental period, if applicable. | +| `beds`, `baths` | Positive numeric counts; omit unknown values. | +| `size` | Formatted area including units. | +| `note` | Why this property is worth a look. | +| `facts` | Array of short verified specs or amenities. | +| `catches` | Array of risks or questions to verify before a tour. | +| `images` | Direct HTTPS photo URLs in listing order; the first is the hero. | +| `links` | Array of `{ "label": "Source", "url": "https://..." }` detail-page links, not search-result URLs. | + +## Procedure + +1. Gather the address, price, specs, photos and canonical detail URL. Distinguish + verified facts from unknowns; do not invent prices, amenities, or photo URLs. +2. Deduplicate portal mirrors of the same property into one card, retaining useful + source links. Keep source dates and availability caveats in the surrounding prose. +3. Emit the `listing` fence for every property presented, including follow-ups and + re-rankings. Keep facts short and put unresolved concerns in `catches`. +4. Check the JSON before sending. This fictional format example illustrates all fields; + replace its values and example URLs with verified listing data: + +```listing +{ + "address": "12 Example Lane", + "price": "$2,400/mo", + "beds": 3, + "baths": 2.5, + "size": "1,600 sqft", + "note": "Fits the requested space and budget.", + "facts": ["12-month lease", "Covered parking"], + "catches": ["Verify pet policy and total move-in fees"], + "images": ["https://example.com/property/front.jpg", "https://example.com/property/kitchen.jpg"], + "links": [{"label": "Listing details", "url": "https://example.com/property/12"}] +} +``` + +## Pitfalls + +- Cards are authored from gathered data, not fetched from a listing URL or embedded portal page. +- A sparse card needs only an address. Omit unknown fields rather than filling them with guesses. +- Use direct remote image URLs, not local paths, data URLs, or search-result pages. + Expired or blocked images disappear from the gallery; the text and links still matter. +- Keep a fence to at most 24 properties, 40 images per property, and 12 entries in + facts, catches and links. Text fields are truncated to 400 characters by the renderer. +- Malformed JSON or a card without identity falls back to a plain code block. + A valid card is not proof that the underlying listing is current or accurate. + +## Verification + +- Every presented property has an address and a verified source link; unknowns are explicit. +- Desktop displays the address, price, specs, facts, catches and links as a native card. +- Photos form a gallery; selecting a photo opens the lightbox. Three or more photos + use a hero-and-supporting-frames mosaic; additional photos remain browsable there. +- If the card fails to render, validate the fence language and JSON, then preserve a + readable Markdown fallback with the same facts and links. diff --git a/website/sidebars.ts b/website/sidebars.ts index ba0598f507..e4e2d43f23 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -538,6 +538,7 @@ const sidebars: SidebarsConfig = { 'user-guide/skills/optional/productivity/productivity-decision-questionnaire', 'user-guide/skills/optional/productivity/productivity-here-now', 'user-guide/skills/optional/productivity/productivity-memento-flashcards', + 'user-guide/skills/optional/productivity/productivity-property-listings', 'user-guide/skills/optional/productivity/productivity-shop', 'user-guide/skills/optional/productivity/productivity-shopify', 'user-guide/skills/optional/productivity/productivity-siyuan',