feat: auto-project-detection + project stages
Three changes: 1. ABB-Space registered as a lead project with stage=lead in Trusted Project State. Projects now have lifecycle awareness (lead/proposition vs active contract vs completed). 2. Extraction no longer drops unregistered project tags. When the LLM extractor sees a conversation about a project not in the registry, it keeps the model's tag on the candidate instead of falling back to empty. This enables auto-detection of new projects/leads from organic conversations. The nightly pipeline surfaces these candidates for triage, where the operator sees "hey, there's a new project called X" and can decide whether to register it. 3. Extraction prompt updated to tell the model: "If the conversation discusses a project NOT in the known list, still tag it — the system will auto-detect it." This removes the artificial ceiling that prevented new project discovery. Updated Case D test: unregistered + unscoped now keeps the model's tag instead of dropping to empty. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@ SYSTEM_PROMPT = """You extract durable memory candidates from LLM conversation t
|
|||||||
|
|
||||||
AtoCore stores two kinds of knowledge:
|
AtoCore stores two kinds of knowledge:
|
||||||
|
|
||||||
A. PROJECT-SPECIFIC: applied decisions, constraints, and architecture for a named project (p04-gigabit, p05-interferometer, p06-polisher, atomizer-v2, atocore). These stay scoped to one project.
|
A. PROJECT-SPECIFIC: applied decisions, constraints, and architecture for a named project. Known projects include p04-gigabit, p05-interferometer, p06-polisher, atomizer-v2, atocore, abb-space. If the conversation discusses a project NOT in this list, still tag it with the project name you identify — the system will auto-detect it as a new project or lead.
|
||||||
|
|
||||||
B. DOMAIN KNOWLEDGE: generalizable engineering insight that was EARNED through project work and is reusable across projects. Tag these with a domain instead of a project.
|
B. DOMAIN KNOWLEDGE: generalizable engineering insight that was EARNED through project work and is reusable across projects. Tag these with a domain instead of a project.
|
||||||
|
|
||||||
@@ -213,9 +213,11 @@ def parse_candidates(raw, interaction_project):
|
|||||||
model_project = str(item.get("project") or "").strip()
|
model_project = str(item.get("project") or "").strip()
|
||||||
domain = str(item.get("domain") or "").strip().lower()
|
domain = str(item.get("domain") or "").strip().lower()
|
||||||
# R9 trust hierarchy: interaction scope always wins when set.
|
# R9 trust hierarchy: interaction scope always wins when set.
|
||||||
|
# For unscoped interactions, keep model's project tag even if
|
||||||
|
# unregistered — the system will detect new projects/leads.
|
||||||
if interaction_project:
|
if interaction_project:
|
||||||
project = interaction_project
|
project = interaction_project
|
||||||
elif model_project and model_project in _known_projects:
|
elif model_project:
|
||||||
project = model_project
|
project = model_project
|
||||||
else:
|
else:
|
||||||
project = ""
|
project = ""
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ _SYSTEM_PROMPT = """You extract durable memory candidates from LLM conversation
|
|||||||
|
|
||||||
AtoCore stores two kinds of knowledge:
|
AtoCore stores two kinds of knowledge:
|
||||||
|
|
||||||
A. PROJECT-SPECIFIC: applied decisions, constraints, and architecture for a named project (p04-gigabit, p05-interferometer, p06-polisher, atomizer-v2, atocore). These stay scoped to one project.
|
A. PROJECT-SPECIFIC: applied decisions, constraints, and architecture for a named project. Known projects include p04-gigabit, p05-interferometer, p06-polisher, atomizer-v2, atocore, abb-space. If the conversation discusses a project NOT in this list, still tag it with the project name you identify — the system will auto-detect it as a new project or lead.
|
||||||
|
|
||||||
B. DOMAIN KNOWLEDGE: generalizable engineering insight that was EARNED through project work and is reusable across projects. Tag these with a domain instead of a project.
|
B. DOMAIN KNOWLEDGE: generalizable engineering insight that was EARNED through project work and is reusable across projects. Tag these with a domain instead of a project.
|
||||||
|
|
||||||
@@ -291,9 +291,20 @@ def _parse_candidates(raw_output: str, interaction: Interaction) -> list[MemoryC
|
|||||||
|
|
||||||
registered_ids = {p.project_id for p in load_project_registry()}
|
registered_ids = {p.project_id for p in load_project_registry()}
|
||||||
resolved = resolve_project_name(model_project)
|
resolved = resolve_project_name(model_project)
|
||||||
project = resolved if resolved in registered_ids else ""
|
if resolved in registered_ids:
|
||||||
|
project = resolved
|
||||||
|
else:
|
||||||
|
# Unregistered project — keep the model's tag so
|
||||||
|
# auto-triage / the operator can see it and decide
|
||||||
|
# whether to register it as a new project or lead.
|
||||||
|
project = model_project
|
||||||
|
log.info(
|
||||||
|
"unregistered_project_detected",
|
||||||
|
model_project=model_project,
|
||||||
|
interaction_id=interaction.id,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
project = ""
|
project = model_project if model_project else ""
|
||||||
else:
|
else:
|
||||||
project = ""
|
project = ""
|
||||||
domain = str(item.get("domain") or "").strip().lower()
|
domain = str(item.get("domain") or "").strip().lower()
|
||||||
|
|||||||
@@ -130,17 +130,17 @@ def test_case_c_unregistered_model_scoped_interaction(tmp_data_dir, project_regi
|
|||||||
assert result[0].project == "p06-polisher"
|
assert result[0].project == "p06-polisher"
|
||||||
|
|
||||||
|
|
||||||
def test_case_d_unregistered_model_unscoped_interaction(tmp_data_dir, project_registry):
|
def test_case_d_unregistered_model_unscoped_keeps_tag(tmp_data_dir, project_registry):
|
||||||
"""Case D: model returns unregistered project, interaction is unscoped.
|
"""Case D: model returns unregistered project, interaction is unscoped.
|
||||||
Falls to empty (not the hallucinated name)."""
|
Keeps the model's tag for auto-project-detection (new behavior)."""
|
||||||
from atocore.models.database import init_db
|
from atocore.models.database import init_db
|
||||||
init_db()
|
init_db()
|
||||||
project_registry(("p06-polisher", ["p06"]))
|
project_registry(("p06-polisher", ["p06"]))
|
||||||
raw = '[{"type": "project", "content": "x", "project": "fake-project-99"}]'
|
raw = '[{"type": "project", "content": "x", "project": "new-lead-project"}]'
|
||||||
interaction = _make_interaction()
|
interaction = _make_interaction()
|
||||||
interaction.project = ""
|
interaction.project = ""
|
||||||
result = _parse_candidates(raw, interaction)
|
result = _parse_candidates(raw, interaction)
|
||||||
assert result[0].project == ""
|
assert result[0].project == "new-lead-project"
|
||||||
|
|
||||||
|
|
||||||
def test_case_e_matching_model_and_interaction(tmp_data_dir, project_registry):
|
def test_case_e_matching_model_and_interaction(tmp_data_dir, project_registry):
|
||||||
|
|||||||
Reference in New Issue
Block a user