fix(desktop): load property card guidance only on demand
This commit is contained in:
@@ -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 "
|
||||
|
||||
76
evals/prompt_footprint/property_guidance.py
Normal file
76
evals/prompt_footprint/property_guidance.py
Normal file
@@ -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()
|
||||
105
optional-skills/productivity/property-listings/SKILL.md
Normal file
105
optional-skills/productivity/property-listings/SKILL.md
Normal file
@@ -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.
|
||||
38
tests/skills/test_property_listings_skill.py
Normal file
38
tests/skills/test_property_listings_skill.py
Normal file
@@ -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()
|
||||
@@ -207,6 +207,7 @@ hermes skills uninstall <skill-name>
|
||||
| [**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. |
|
||||
|
||||
@@ -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.
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user