Explorar el Código

Changed opportunity focus

Efren Yevale Varela hace 3 semanas
padre
commit
c808038924

+ 1 - 1
config.example.toml

@@ -99,7 +99,7 @@ trim_block_turns               = 4
 max_tool_iterations            = 3
 system_prompt_chat             = "You are a concise voice assistant. Reply in 1-3 sentences."
 system_prompt_command          = "You translate a spoken request into a single tool call when one applies."
-triage_system_prompt           = "You maintain a living brief of a business conversation. The caller sends CURRENT LEDGER plus CURRENT BRIEF plus NEW UTTERANCES. Reply with exactly two sections in this order: NEW FACTS: first, then BRIEF:. Facts go first so a truncated reply still preserves extraction (truncation safety). In NEW FACTS:, emit one fact per line as '- <fact>', or the single line 'none' when the new utterances add no new facts. Never repeat facts already listed in CURRENT LEDGER. In BRIEF:, return the UPDATED brief as plain text only, with no markdown fences and no JSON. Open with a Summary section: a rolling overview of the conversation so far, one to a few sentences for a short session and growing to a short paragraph or two as the session extends. Preserve prior content unless the new utterances contradict it; merge duplicates; prune stale items; keep owners, responsibilities, and deadlines explicit. Use short sections: Summary, Opportunities, Blockers/Risks, Responsibilities, Decisions, Action Items, Deadlines, Open Questions. Never silently delete Action Items, Open Questions, or Decisions lines. When an action completes or a question is answered, do NOT drop the line — move it to Decisions as 'Done: <original> — resolved: <what happened>' (or keep it in place with a 'Done (...)' suffix) AND list the resolution as a NEW FACTS bullet. Prune only exact duplicates, never resolved history. Never emit a 'Prior Details:' section. Output the two sections directly; never emit tool calls, function-call syntax, or special wrapper tokens."
+triage_system_prompt           = "You maintain a living brief of a business conversation. The caller sends CURRENT LEDGER plus CURRENT BRIEF plus NEW UTTERANCES. Reply with exactly two sections in this order: NEW FACTS: first, then BRIEF:. Facts go first so a truncated reply still preserves extraction (truncation safety). In NEW FACTS:, emit one fact per line as '- <fact>', or the single line 'none' when the new utterances add no new facts. Never repeat facts already listed in CURRENT LEDGER. In BRIEF:, return the UPDATED brief as plain text only, with no markdown fences and no JSON. Open with a Summary section: a rolling overview of the conversation so far, one to a few sentences for a short session and growing to a short paragraph or two as the session extends. Preserve prior content unless the new utterances contradict it; merge duplicates; prune stale items; keep owners, responsibilities, and deadlines explicit. Use short sections: Summary, Opportunities, Blockers/Risks, Responsibilities, Decisions, Action Items, Deadlines, Open Questions. Never silently delete Action Items, Open Questions, or Decisions lines. When an action completes or a question is answered, do NOT drop the line — move it to Decisions as 'Done: <original> — resolved: <what happened>' (or keep it in place with a 'Done (...)' suffix) AND list the resolution as a NEW FACTS bullet. Prune only exact duplicates, never resolved history. Lead the Summary with currently-open opportunities first, then the rest. Keep the Opportunities section the most complete and current — do not let Blockers/Risks crowd it out; other sections stay unchanged. Never emit a 'Prior Details:' section. Output the two sections directly; never emit tool calls, function-call syntax, or special wrapper tokens."
 
 [triage]
 enabled                   = false

+ 1 - 1
docs/CONFIGURATION.md

@@ -123,7 +123,7 @@ trim_block_turns         = 4                # fallback block trimming, not per-t
 max_tool_iterations      = 3
 system_prompt_chat       = "You are a concise voice assistant. Reply in 1-3 sentences."
 system_prompt_command    = "You translate a spoken request into a single tool call when one applies."
-triage_system_prompt     = "You maintain a living brief of a business conversation. The caller sends CURRENT LEDGER plus CURRENT BRIEF plus NEW UTTERANCES. Reply with exactly two sections in this order: NEW FACTS: first, then BRIEF:. Facts go first so a truncated reply still preserves extraction (truncation safety). In NEW FACTS:, emit one fact per line as '- <fact>', or the single line 'none' when the new utterances add no new facts. Never repeat facts already listed in CURRENT LEDGER. In BRIEF:, return the UPDATED brief as plain text only, with no markdown fences and no JSON. Open with a Summary section: a rolling overview of the conversation so far, one to a few sentences for a short session and growing to a short paragraph or two as the session extends. Preserve prior content unless the new utterances contradict it; merge duplicates; prune stale items; keep owners, responsibilities, and deadlines explicit. Use short sections: Summary, Opportunities, Blockers/Risks, Responsibilities, Decisions, Action Items, Deadlines, Open Questions. Never silently delete Action Items, Open Questions, or Decisions lines. When an action completes or a question is answered, do NOT drop the line — move it to Decisions as 'Done: <original> — resolved: <what happened>' (or keep it in place with a 'Done (...)' suffix) AND list the resolution as a NEW FACTS bullet. Prune only exact duplicates, never resolved history. Never emit a 'Prior Details:' section. Output the two sections directly; never emit tool calls, function-call syntax, or special wrapper tokens."
+triage_system_prompt     = "You maintain a living brief of a business conversation. The caller sends CURRENT LEDGER plus CURRENT BRIEF plus NEW UTTERANCES. Reply with exactly two sections in this order: NEW FACTS: first, then BRIEF:. Facts go first so a truncated reply still preserves extraction (truncation safety). In NEW FACTS:, emit one fact per line as '- <fact>', or the single line 'none' when the new utterances add no new facts. Never repeat facts already listed in CURRENT LEDGER. In BRIEF:, return the UPDATED brief as plain text only, with no markdown fences and no JSON. Open with a Summary section: a rolling overview of the conversation so far, one to a few sentences for a short session and growing to a short paragraph or two as the session extends. Preserve prior content unless the new utterances contradict it; merge duplicates; prune stale items; keep owners, responsibilities, and deadlines explicit. Use short sections: Summary, Opportunities, Blockers/Risks, Responsibilities, Decisions, Action Items, Deadlines, Open Questions. Never silently delete Action Items, Open Questions, or Decisions lines. When an action completes or a question is answered, do NOT drop the line — move it to Decisions as 'Done: <original> — resolved: <what happened>' (or keep it in place with a 'Done (...)' suffix) AND list the resolution as a NEW FACTS bullet. Prune only exact duplicates, never resolved history. Lead the Summary with currently-open opportunities first, then the rest. Keep the Opportunities section the most complete and current — do not let Blockers/Risks crowd it out; other sections stay unchanged. Never emit a 'Prior Details:' section. Output the two sections directly; never emit tool calls, function-call syntax, or special wrapper tokens."
 
 [triage]                                    # Display-only signal panel (Laya lane; no socket, no extra process)
 enabled              = false                # Master gate: false = no lane, no panel, behavior identical to today

+ 2 - 1
docs/TRIAGE.md

@@ -95,7 +95,8 @@ settled turn (any mode), every periodic_s while speech keeps accumulating, or a
    `[escalated]` markers — e.g. `- blocker (p=0.91, flagged, gate=0.90): An obstacle ...`
    or `- decision_status (choice): selected made (conf 0.90) — What is the state ...?
    Options: a decision was reached`. Low-value `noul` noise (`p < 0.30`) is omitted,
-   except `substantive_update` which is always kept for gate context; `choice`/`score`
+   except `substantive_update` and `opportunity` which are always kept; `opportunity`
+   lines sort first so the brief leads with open opportunities. `choice`/`score`
    items are always kept with their uncertainty flagged. The brief engine is **stateless**: it
    sends exactly two messages per call — a static system prompt and ONE user turn containing
    `CURRENT LEDGER` (the recorded facts), `CURRENT BRIEF` (the volatile narrative), and

+ 19 - 4
src/stt_daemon/assistant/triage.py

@@ -424,9 +424,11 @@ class TriageAnalyzer:
         """Render each ordered signal as a "- " words line; never raises.
 
         Filters low-value noise: noul items with p < 0.30 are omitted,
-        except the ``substantive_update`` gate arm which is always kept for
-        threshold context. Choice/score items are always kept (flagged
-        marker explains uncertainty). Original order is preserved.
+        except the ``substantive_update`` gate arm and the ``opportunity``
+        arm, which are always kept (threshold context plus low-traction
+        idea visibility). Choice/score items are always kept (flagged
+        marker explains uncertainty). Opportunity line(s) are stably
+        partitioned first, preserving relative order otherwise.
         """
         lines = []
         try:
@@ -443,7 +445,10 @@ class TriageAnalyzer:
                     item = items.get(qid)
                     if not isinstance(item, dict):
                         continue
-                    if item.get("kind") == "noul" and qid != "substantive_update":
+                    if item.get("kind") == "noul" and qid not in (
+                        "substantive_update",
+                        "opportunity",
+                    ):
                         prob = _number(item.get("p"))
                         if prob is not None and prob < 0.30:
                             continue
@@ -455,6 +460,13 @@ class TriageAnalyzer:
                     continue
         except Exception:
             return []
+        try:
+            opp = [line for line in lines if line.startswith("- opportunity")]
+            if opp:
+                rest = [line for line in lines if not line.startswith("- opportunity")]
+                return opp + rest
+        except Exception:
+            pass
         return lines
 
     def build_brief_prompt(self, deltas, signals) -> str:
@@ -504,6 +516,9 @@ class TriageAnalyzer:
             "to Decisions as 'Done: <original> — resolved: <what happened>' "
             "(or keep it with a 'Done (...)' suffix) and record the resolution "
             "as a NEW FACTS bullet; prune only exact duplicates."
+            " Lead the Summary with currently-open opportunities first, and keep "
+            "the Opportunities section the most complete and current without "
+            "dropping other sections."
         )
         return "\n\n".join(parts)
 

+ 4 - 1
src/stt_daemon/config.py

@@ -171,7 +171,10 @@ class LlmConfig:
         "is answered, do NOT drop the line — move it to Decisions as "
         "'Done: <original> — resolved: <what happened>' (or keep it in place "
         "with a 'Done (...)' suffix) AND list the resolution as a NEW FACTS "
-        "bullet. Prune only exact duplicates, never resolved history. "
+        "bullet. Prune only exact duplicates, never resolved history. Lead the "
+        "Summary with currently-open opportunities first, then the rest. Keep the "
+        "Opportunities section the most complete and current — do not let "
+        "Blockers/Risks crowd it out; other sections stay unchanged. "
         "Never emit a 'Prior Details:' section. Output "
         "the two sections directly; never emit tool calls, function-call syntax, "
         "or special wrapper tokens."

+ 4 - 1
tests/test_config.py

@@ -696,7 +696,10 @@ def test_llm_slot_and_brief_prompt_defaults():
         "is answered, do NOT drop the line — move it to Decisions as "
         "'Done: <original> — resolved: <what happened>' (or keep it in place "
         "with a 'Done (...)' suffix) AND list the resolution as a NEW FACTS "
-        "bullet. Prune only exact duplicates, never resolved history. "
+        "bullet. Prune only exact duplicates, never resolved history. Lead the "
+        "Summary with currently-open opportunities first, then the rest. Keep the "
+        "Opportunities section the most complete and current — do not let "
+        "Blockers/Risks crowd it out; other sections stay unchanged. "
         "Never emit a 'Prior Details:' section. Output "
         "the two sections directly; never emit tool calls, function-call syntax, "
         "or special wrapper tokens."

+ 50 - 0
tests/test_triage.py

@@ -1244,3 +1244,53 @@ def test_signal_lines_never_raises_on_malformed(tmp_path):
     assert analyzer._signal_lines({}) == []  # type: ignore[dict-item]
     assert analyzer._signal_lines({"order": "nope", "items": "nope"}) == []  # type: ignore[dict-item]
     assert analyzer._signal_lines({"order": ["ghost"], "items": {}}) == []
+
+
+def test_signal_lines_opportunity_kept_below_threshold(tmp_path):
+    questions = _rich_questions()
+    questions["opportunity"] = {"type": "noul", "instructions": "A new lead."}
+    analyzer = _analyzer(tmp_path, questions)
+    items = _rich_signals()["items"]
+    items["opportunity"] = {
+        "kind": "noul",
+        "p": 0.10,
+        "confidence": 0.9,
+        "answer_confidence": 0.9,
+        "gate": 0.9,
+        "flagged": False,
+    }
+    signals = {"order": ["blocker", "opportunity", "churn_risk"],
+               "items": items, "escalated": False}
+    lines = analyzer._signal_lines(signals)
+    assert any("opportunity" in line for line in lines)
+    assert not any("churn_risk" in line for line in lines)
+
+
+def test_signal_lines_opportunity_ordered_first(tmp_path):
+    questions = _rich_questions()
+    questions["opportunity"] = {"type": "noul", "instructions": "A new lead."}
+    analyzer = _analyzer(tmp_path, questions)
+    items = _rich_signals()["items"]
+    items["opportunity"] = {
+        "kind": "noul",
+        "p": 0.80,
+        "confidence": 0.9,
+        "answer_confidence": 0.9,
+        "gate": 0.9,
+        "flagged": False,
+    }
+    signals = {"order": ["blocker", "severity", "opportunity"],
+               "items": items, "escalated": False}
+    lines = analyzer._signal_lines(signals)
+    assert lines[0].startswith("- opportunity")
+    # relative order of the rest preserved
+    rest_idx = [i for i, line in enumerate(lines) if "blocker" in line][0]
+    sev_idx = [i for i, line in enumerate(lines) if "severity" in line][0]
+    assert rest_idx < sev_idx
+
+
+def test_build_ledger_prompt_contains_opportunity_lead_rule(tmp_path):
+    analyzer = _analyzer(tmp_path, _rich_questions())
+    prompt = analyzer.build_ledger_prompt("Summary: x", [], ["hi"], None)
+    assert "currently-open opportunities first" in prompt
+    assert "Opportunities section the most complete" in prompt