Skip to content

Commit 5b88d57

Browse files
committed
fix(timeline): use strict agent attributes schema
1 parent 77da763 commit 5b88d57

5 files changed

Lines changed: 45 additions & 3 deletions

File tree

‎backends/advanced/src/advanced_omi_backend/services/timeline/contracts.py‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ class AgentAssertion(BaseModel):
4949
evidence_ids: list[str] = Field(min_length=1)
5050

5151

52+
class AgentAttribute(BaseModel):
53+
"""A strict-schema-safe episode attribute.
54+
55+
Codex structured outputs cannot represent an object with arbitrary keys, so the
56+
agent emits key/value entries and Chronicle materializes them as an object when
57+
publishing the episode.
58+
"""
59+
60+
key: str = Field(min_length=1, max_length=80)
61+
value: str = Field(max_length=500)
62+
63+
5264
class AgentEpisode(BaseModel):
5365
kind: str = Field(min_length=1, max_length=80)
5466
title: str = Field(min_length=1, max_length=160)
@@ -59,7 +71,7 @@ class AgentEpisode(BaseModel):
5971
activity_mode: Literal["foreground", "background", "ambient", "idle"]
6072
confidence: float = Field(ge=0, le=1)
6173
entities: list[str] = Field(default_factory=list)
62-
attributes: dict[str, Any] = Field(default_factory=dict)
74+
attributes: list[AgentAttribute] = Field(default_factory=list)
6375
assertions: list[AgentAssertion] = Field(default_factory=list)
6476
evidence_ids: list[str] = Field(min_length=1)
6577
related_conversation_ids: list[str] = Field(default_factory=list)

‎backends/advanced/src/advanced_omi_backend/services/timeline/discovery.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ async def _publish(
183183
confidence=episode.confidence,
184184
activity_mode=episode.activity_mode,
185185
entities=episode.entities,
186-
attributes=episode.attributes,
186+
attributes={item.key: item.value for item in episode.attributes},
187187
assertions=[
188188
TimelineAssertion(**assertion.model_dump())
189189
for assertion in episode.assertions

‎backends/advanced/src/advanced_omi_backend/services/timeline/prompt.py‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,18 @@
3131
},
3232
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
3333
"entities": {"type": "array", "items": {"type": "string"}},
34-
"attributes": {"type": "object"},
34+
"attributes": {
35+
"type": "array",
36+
"items": {
37+
"type": "object",
38+
"additionalProperties": False,
39+
"properties": {
40+
"key": {"type": "string"},
41+
"value": {"type": "string"},
42+
},
43+
"required": ["key", "value"],
44+
},
45+
},
3546
"assertions": {
3647
"type": "array",
3748
"items": {
@@ -131,5 +142,6 @@ def build_prompt(output_path: str) -> str:
131142
- Every important boundary/assertion must cite supplied evidence IDs.
132143
- Never invent an evidence ID. Include every window ID exactly once in covered_window_ids.
133144
- Salience is display value, not confidence.
145+
- Express optional episode metadata as short string key/value entries in `attributes`.
134146
- Prefer a few coherent episodes over arbitrary periodic fragments.
135147
"""

‎backends/advanced/tests/test_timeline_executor.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from advanced_omi_backend.services.timeline.contracts import (
66
AgentAssertion,
7+
AgentAttribute,
78
AgentEpisode,
89
TimelineAgentResult,
910
TimelineCoverageWindow,
@@ -57,6 +58,7 @@ def _result(evidence_id: str = "observation:one") -> TimelineAgentResult:
5758
salience="routine",
5859
activity_mode="background",
5960
confidence=0.9,
61+
attributes=[AgentAttribute(key="media", value="Terminator")],
6062
evidence_ids=[evidence_id],
6163
assertions=[
6264
AgentAssertion(
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from advanced_omi_backend.services.timeline.prompt import OUTPUT_SCHEMA
2+
3+
4+
def _assert_strict_objects(schema: object) -> None:
5+
if isinstance(schema, dict):
6+
if schema.get("type") == "object":
7+
assert schema.get("additionalProperties") is False
8+
for value in schema.values():
9+
_assert_strict_objects(value)
10+
elif isinstance(schema, list):
11+
for value in schema:
12+
_assert_strict_objects(value)
13+
14+
15+
def test_timeline_output_schema_has_no_open_objects():
16+
_assert_strict_objects(OUTPUT_SCHEMA)

0 commit comments

Comments
 (0)