Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG_SINCE_YESTERDAY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Changelog Since Yesterday's Submission

Assuming yesterday’s ClawHub submission corresponds to commit `6942cbd` (release prep for v0.1.10).

## Included commits (`6942cbd..4a58db3`)

### 1) CI safety fix
- `75f71c5` — **fix(ci): narrow low-signal filter to avoid dropping valid training pairs**

### 2) Draft quality improvements
- `fe80fd7` — **feat(quality): implement sender-type style anchors and exemplar cache**
- Adds sender-type style anchoring in draft prompting.
- Adds exemplar cache logic for more consistent draft selection.

### 3) Persistence + onboarding defaults
- `d336c5b` — **feat(YouOS): Implement persistent exemplar cache and quickstart default; fix syntax**
- Persists exemplar cache in SQLite (`exemplar_cache` table) across restarts.
- Wires cache read/write/clear through API + generation paths.
- Sets quickstart-first onboarding defaults.
- Fixes syntax issue in generation service.

### 4) Dashboard metrics visibility
- `4a58db3` — **feat(YouOS): Display edit-reduction metrics in dashboard**
- Surfaces edit-distance and high-rating deltas in stats UI.

## Test status (targeted)
- `.venv/bin/pytest -q tests/test_style_anchor_cache.py tests/test_config.py tests/test_setup_wizard.py tests/test_generation_improvements.py`
- Result: **35 passed**

## Files touched in this delta
- `app/api/feedback_routes.py`
- `app/api/stream_routes.py`
- `app/db/bootstrap.py`
- `app/generation/service.py`
- `scripts/setup_wizard.py`
- `templates/stats.html`
- `youos_config.yaml`
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ Configuration is in `pyproject.toml` (line length 100, Python 3.11 target).
3. Add tests for new functionality
4. Ensure all tests pass: `python -m pytest tests/ -q`
5. Ensure linting passes: `ruff check .`
6. Submit a PR with a clear description of the change
6. Run the release checklist in `docs/RELEASE_GUARDRAILS.md` for any user-facing/docs/submission change
7. Submit a PR with a clear description of the change

## Architecture overview

Expand Down
14 changes: 10 additions & 4 deletions PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
- GitHub account authenticated
- All CI checks passing

## Steps
1. Bump version in clawhub.json and CHANGELOG.md
## Steps (default release prep flow)
1. Bump version in `clawhub.json`, `pyproject.toml`, and `CHANGELOG.md`
2. Commit: `git commit -m "chore: bump version to X.Y.Z"`
3. Tag: `git tag vX.Y.Z && git push origin vX.Y.Z`
4. Publish: `clawhub publish ./`
5. Verify on https://clawhub.com/skills/youos
4. Build a **minimal allowlist release folder** (default):
- `./scripts/prepare_clawhub_release.sh`
- Optional custom output: `./scripts/prepare_clawhub_release.sh ~/Documents/youos-release-X.Y.Z`
- This script includes only: `app/`, `clawhub.json`, `configs/`, `PRIVACY.md`, `pyproject.toml`, `README.md`, `scripts/`, `SKILL.md`
5. Publish from that folder (not repo root):
- `cd ~/Documents/youos-release-X.Y.Z`
- `clawhub publish ./`
6. Verify on https://clawhub.com/skills/youos

## What clawhub publish does
- Reads SKILL.md and clawhub.json
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ Gmail (sent mail) Your feedback
- Learns your writing style — richer persona: bullet point rate, directness score, sentence length, paragraph style; EWMA-weighted toward recent emails
- Persona re-analysis is incremental (recent 90 days × 3 weight), with full weekly refresh; confidence intervals (p25/p75) shown in prompts
- Per-sender-type personas: different voice, length, greeting, and closing for internal, external client, and personal contacts
- Sender-type style anchors: explicit prompt slot (`[STYLE ANCHOR — internal|client|personal]`) to stabilize first-draft tone by audience
- Per-account corpus isolation — drafts for work emails draw from work history; personal from personal
- Greets people by first name, closes in your style — greeting and closing injected from persona config per contact type
- Classifies multi-intent (meeting + urgent, etc.), boosts matching exemplars; per-intent reply length calibrated from corpus
- Drafts grounded in score-ranked few-shot exemplars (confidence-annotated, thread-deduplicated); exemplar reply text preserved (600 chars), inbound trimmed (400)
- Exemplar cache by intent+sender-type (TTL + feedback-triggered invalidation) improves consistency and reduces repeated ranking churn
- Prompt token budget enforced — exemplars auto-trimmed if prompt exceeds 2000 tokens
- Confidence thresholds are relative (mean±σ of retrieval scores), not hardcoded
- Subject line + topic-aware retrieval; FTS queries expanded with email vocabulary synonyms
Expand Down
11 changes: 8 additions & 3 deletions app/api/feedback_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from app.core.facts_extractor import extract_and_save
from app.core.rate_limit import RATE_LIMIT_RESPONSE, draft_limiter
from app.db.bootstrap import resolve_sqlite_path
from app.generation.service import DraftRequest, generate_draft
from app.generation.service import DraftRequest, clear_exemplar_cache, generate_draft

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -198,7 +198,7 @@ def feedback_generate(body: GenerateBody, request: Request) -> dict:
"""INSERT INTO draft_history
(inbound_text, sender, generated_draft, confidence, model_used, retrieval_method)
VALUES (?, ?, ?, ?, ?, ?)""",
(body.inbound_text, body.sender, response.draft, response.confidence, response.model_used, response.retrieval_method),
(body.inbound_text, body.sender, response.draft, response.confidence, response.model_used, f"{response.retrieval_method}|cache_hit={int(response.exemplar_cache_hit)}|cache_key={response.exemplar_cache_key or ''}"),
)
conn.commit()
finally:
Expand All @@ -213,6 +213,8 @@ def feedback_generate(body: GenerateBody, request: Request) -> dict:
"confidence_reason": response.confidence_reason,
"confidence_warning": response.confidence == "low",
"suggested_subject": response.suggested_subject,
"exemplar_cache_hit": response.exemplar_cache_hit,
"exemplar_cache_key": response.exemplar_cache_key,
}


Expand Down Expand Up @@ -273,9 +275,12 @@ def feedback_submit(body: SubmitBody, request: Request) -> dict:
quality_score = max(0.3, min(1.3, quality_score))
conn.execute("UPDATE reply_pairs SET quality_score = ? WHERE id = ?", (round(quality_score, 4), rp_id))
conn.commit()
# Invalidate exemplar cache on successful quality update
clear_exemplar_cache(database_url=request.app.state.settings.database_url)
except Exception:
logger.warning("Failed to update quality_score for reply pair", exc_info=True)
logger.warning("Failed to update quality_score for reply pair or clear cache", exc_info=True)

clear_exemplar_cache(database_url=request.app.state.settings.database_url)
total = conn.execute("SELECT COUNT(*) FROM feedback_pairs").fetchone()[0]
finally:
conn.close()
Expand Down
19 changes: 18 additions & 1 deletion app/api/stream_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
_precedent_summary,
_score_confidence,
assemble_prompt,
_apply_cached_order,
_get_cached_exemplar_ids,
_top_exemplar_source_ids,
_update_exemplar_cache,
generate_draft,
lookup_sender_profile,
)
Expand Down Expand Up @@ -59,6 +63,18 @@ def _stream_generate(body: StreamBody, settings):
)

reply_pairs = retrieval_response.reply_pairs

# Apply exemplar caching (read + write)
from app.core.intent import classify_intents_multi
intents = classify_intents_multi(clean_inbound)
detected_intent = intents[0]

cached_ids, exemplar_cache_hit, exemplar_cache_key = _get_cached_exemplar_ids(detected_intent, sender_type_hint, database_url=settings.database_url)
reply_pairs = _apply_cached_order(reply_pairs, cached_ids)

selected_ids = _top_exemplar_source_ids(reply_pairs)
_update_exemplar_cache(detected_intent, sender_type_hint, selected_ids, database_url=settings.database_url)

confidence, _ = _score_confidence(reply_pairs)
precedent_used = [_precedent_summary(rp) for rp in reply_pairs]
detected_mode = retrieval_response.detected_mode
Expand All @@ -80,6 +96,7 @@ def _stream_generate(body: StreamBody, settings):
detected_mode=detected_mode,
tone_hint=body.tone_hint,
sender_context=sender_context,
sender_type=sender_type_hint,
user_prompt=body.user_prompt,
)

Expand Down Expand Up @@ -117,7 +134,7 @@ def _stream_generate(body: StreamBody, settings):
except Exception as exc:
yield f"data: {json.dumps({'token': f'[generation failed: {exc}]'})}\n\n"

yield f"data: {json.dumps({'done': True, 'confidence': confidence, 'precedent_used': precedent_used})}\n\n"
yield f"data: {json.dumps({'done': True, 'confidence': confidence, 'precedent_used': precedent_used, 'exemplar_cache_hit': exemplar_cache_hit, 'exemplar_cache_key': exemplar_cache_key})}\n\n"


@router.post("/stream")
Expand Down
11 changes: 11 additions & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ def get_autoresearch_iterations(config: dict[str, Any] | None = None) -> int:
return int(cfg.get("autoresearch", {}).get("iterations", 80))


def get_persona_mode_config(sender_type: str, config: dict[str, Any] | None = None) -> dict[str, Any]:
cfg = config or load_config()
return cfg.get("persona", {}).get("modes", {}).get(sender_type, {})


def get_persona_style_anchor(sender_type: str, config: dict[str, Any] | None = None) -> str | None:
cfg = config or load_config()
mode_config = get_persona_mode_config(sender_type, config)
return mode_config.get("style_anchor")


def get_ollama_config(config: dict[str, Any] | None = None) -> dict[str, Any]:
cfg = config or load_config()
return cfg.get("model", {}).get("ollama", {})
Expand Down
13 changes: 13 additions & 0 deletions app/db/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def bootstrap_database() -> Path:
_migrate_sender_profiles(connection)
_migrate_memory(connection)
_migrate_review_streaks(connection)
_migrate_exemplar_cache(connection)
_populate_fts(connection)
connection.commit()
finally:
Expand Down Expand Up @@ -147,3 +148,15 @@ def _populate_fts(connection: sqlite3.Connection) -> None:
)
except Exception:
pass


def _migrate_exemplar_cache(connection: sqlite3.Connection) -> None:
"""Create persistent exemplar cache table if it doesn't exist."""
connection.execute("""
CREATE TABLE IF NOT EXISTS exemplar_cache (
cache_key TEXT PRIMARY KEY,
source_ids_json TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
""")
connection.execute("CREATE INDEX IF NOT EXISTS idx_exemplar_cache_updated ON exemplar_cache(updated_at)")
Loading
Loading