agno 2.2.13__py3-none-any.whl → 2.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agno/agent/__init__.py +6 -0
- agno/agent/agent.py +5252 -3145
- agno/agent/remote.py +525 -0
- agno/api/api.py +2 -0
- agno/client/__init__.py +3 -0
- agno/client/a2a/__init__.py +10 -0
- agno/client/a2a/client.py +554 -0
- agno/client/a2a/schemas.py +112 -0
- agno/client/a2a/utils.py +369 -0
- agno/client/os.py +2669 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/manager.py +2 -2
- agno/db/base.py +927 -6
- agno/db/dynamo/dynamo.py +788 -2
- agno/db/dynamo/schemas.py +128 -0
- agno/db/dynamo/utils.py +26 -3
- agno/db/firestore/firestore.py +674 -50
- agno/db/firestore/schemas.py +41 -0
- agno/db/firestore/utils.py +25 -10
- agno/db/gcs_json/gcs_json_db.py +506 -3
- agno/db/gcs_json/utils.py +14 -2
- agno/db/in_memory/in_memory_db.py +203 -4
- agno/db/in_memory/utils.py +14 -2
- agno/db/json/json_db.py +498 -2
- agno/db/json/utils.py +14 -2
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/utils.py +19 -0
- agno/db/migrations/v1_to_v2.py +54 -16
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +977 -0
- agno/db/mongo/async_mongo.py +1013 -39
- agno/db/mongo/mongo.py +684 -4
- agno/db/mongo/schemas.py +48 -0
- agno/db/mongo/utils.py +17 -0
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2958 -0
- agno/db/mysql/mysql.py +722 -53
- agno/db/mysql/schemas.py +77 -11
- agno/db/mysql/utils.py +151 -8
- agno/db/postgres/async_postgres.py +1254 -137
- agno/db/postgres/postgres.py +2316 -93
- agno/db/postgres/schemas.py +153 -21
- agno/db/postgres/utils.py +22 -7
- agno/db/redis/redis.py +531 -3
- agno/db/redis/schemas.py +36 -0
- agno/db/redis/utils.py +31 -15
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +20 -9
- agno/db/singlestore/schemas.py +70 -1
- agno/db/singlestore/singlestore.py +737 -74
- agno/db/singlestore/utils.py +13 -3
- agno/db/sqlite/async_sqlite.py +1069 -89
- agno/db/sqlite/schemas.py +133 -1
- agno/db/sqlite/sqlite.py +2203 -165
- agno/db/sqlite/utils.py +21 -11
- agno/db/surrealdb/models.py +25 -0
- agno/db/surrealdb/surrealdb.py +603 -1
- agno/db/utils.py +60 -0
- agno/eval/__init__.py +26 -3
- agno/eval/accuracy.py +25 -12
- agno/eval/agent_as_judge.py +871 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +10 -4
- agno/eval/reliability.py +22 -13
- agno/eval/utils.py +2 -1
- agno/exceptions.py +42 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +13 -2
- agno/knowledge/__init__.py +4 -0
- agno/knowledge/chunking/code.py +90 -0
- agno/knowledge/chunking/document.py +65 -4
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/markdown.py +102 -11
- agno/knowledge/chunking/recursive.py +2 -2
- agno/knowledge/chunking/semantic.py +130 -48
- agno/knowledge/chunking/strategy.py +18 -0
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/knowledge/embedder/mistral.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/openai.py +16 -12
- agno/knowledge/filesystem.py +412 -0
- agno/knowledge/knowledge.py +4261 -1199
- agno/knowledge/protocol.py +134 -0
- agno/knowledge/reader/arxiv_reader.py +3 -2
- agno/knowledge/reader/base.py +9 -7
- agno/knowledge/reader/csv_reader.py +91 -42
- agno/knowledge/reader/docx_reader.py +9 -10
- agno/knowledge/reader/excel_reader.py +225 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
- agno/knowledge/reader/firecrawl_reader.py +3 -2
- agno/knowledge/reader/json_reader.py +16 -22
- agno/knowledge/reader/markdown_reader.py +15 -14
- agno/knowledge/reader/pdf_reader.py +33 -28
- agno/knowledge/reader/pptx_reader.py +9 -10
- agno/knowledge/reader/reader_factory.py +135 -1
- agno/knowledge/reader/s3_reader.py +8 -16
- agno/knowledge/reader/tavily_reader.py +3 -3
- agno/knowledge/reader/text_reader.py +15 -14
- agno/knowledge/reader/utils/__init__.py +17 -0
- agno/knowledge/reader/utils/spreadsheet.py +114 -0
- agno/knowledge/reader/web_search_reader.py +8 -65
- agno/knowledge/reader/website_reader.py +16 -13
- agno/knowledge/reader/wikipedia_reader.py +36 -3
- agno/knowledge/reader/youtube_reader.py +3 -2
- agno/knowledge/remote_content/__init__.py +33 -0
- agno/knowledge/remote_content/config.py +266 -0
- agno/knowledge/remote_content/remote_content.py +105 -17
- agno/knowledge/utils.py +76 -22
- agno/learn/__init__.py +71 -0
- agno/learn/config.py +463 -0
- agno/learn/curate.py +185 -0
- agno/learn/machine.py +725 -0
- agno/learn/schemas.py +1114 -0
- agno/learn/stores/__init__.py +38 -0
- agno/learn/stores/decision_log.py +1156 -0
- agno/learn/stores/entity_memory.py +3275 -0
- agno/learn/stores/learned_knowledge.py +1583 -0
- agno/learn/stores/protocol.py +117 -0
- agno/learn/stores/session_context.py +1217 -0
- agno/learn/stores/user_memory.py +1495 -0
- agno/learn/stores/user_profile.py +1220 -0
- agno/learn/utils.py +209 -0
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +223 -8
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +434 -59
- agno/models/aws/bedrock.py +121 -20
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +10 -6
- agno/models/azure/openai_chat.py +33 -10
- agno/models/base.py +1162 -561
- agno/models/cerebras/cerebras.py +120 -24
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +65 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +959 -89
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +48 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +88 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +24 -5
- agno/models/meta/llama.py +40 -13
- agno/models/meta/llama_openai.py +22 -21
- agno/models/metrics.py +12 -0
- agno/models/mistral/mistral.py +8 -4
- agno/models/n1n/__init__.py +3 -0
- agno/models/n1n/n1n.py +57 -0
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/__init__.py +2 -0
- agno/models/ollama/chat.py +17 -6
- agno/models/ollama/responses.py +100 -0
- agno/models/openai/__init__.py +2 -0
- agno/models/openai/chat.py +117 -26
- agno/models/openai/open_responses.py +46 -0
- agno/models/openai/responses.py +110 -32
- agno/models/openrouter/__init__.py +2 -0
- agno/models/openrouter/openrouter.py +67 -2
- agno/models/openrouter/responses.py +146 -0
- agno/models/perplexity/perplexity.py +19 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +19 -2
- agno/models/response.py +20 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/claude.py +124 -4
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +467 -137
- agno/os/auth.py +253 -5
- agno/os/config.py +22 -0
- agno/os/interfaces/a2a/a2a.py +7 -6
- agno/os/interfaces/a2a/router.py +635 -26
- agno/os/interfaces/a2a/utils.py +32 -33
- agno/os/interfaces/agui/agui.py +5 -3
- agno/os/interfaces/agui/router.py +26 -16
- agno/os/interfaces/agui/utils.py +97 -57
- agno/os/interfaces/base.py +7 -7
- agno/os/interfaces/slack/router.py +16 -7
- agno/os/interfaces/slack/slack.py +7 -7
- agno/os/interfaces/whatsapp/router.py +35 -7
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/interfaces/whatsapp/whatsapp.py +11 -8
- agno/os/managers.py +326 -0
- agno/os/mcp.py +652 -79
- agno/os/middleware/__init__.py +4 -0
- agno/os/middleware/jwt.py +718 -115
- agno/os/middleware/trailing_slash.py +27 -0
- agno/os/router.py +105 -1558
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +655 -0
- agno/os/routers/agents/schema.py +288 -0
- agno/os/routers/components/__init__.py +3 -0
- agno/os/routers/components/components.py +475 -0
- agno/os/routers/database.py +155 -0
- agno/os/routers/evals/evals.py +111 -18
- agno/os/routers/evals/schemas.py +38 -5
- agno/os/routers/evals/utils.py +80 -11
- agno/os/routers/health.py +3 -3
- agno/os/routers/knowledge/knowledge.py +284 -35
- agno/os/routers/knowledge/schemas.py +14 -2
- agno/os/routers/memory/memory.py +274 -11
- agno/os/routers/memory/schemas.py +44 -3
- agno/os/routers/metrics/metrics.py +30 -15
- agno/os/routers/metrics/schemas.py +10 -6
- agno/os/routers/registry/__init__.py +3 -0
- agno/os/routers/registry/registry.py +337 -0
- agno/os/routers/session/session.py +143 -14
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +550 -0
- agno/os/routers/teams/schema.py +280 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +549 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +757 -0
- agno/os/routers/workflows/schema.py +139 -0
- agno/os/schema.py +157 -584
- agno/os/scopes.py +469 -0
- agno/os/settings.py +3 -0
- agno/os/utils.py +574 -185
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +102 -2
- agno/reasoning/default.py +6 -7
- agno/reasoning/gemini.py +87 -3
- agno/reasoning/groq.py +109 -2
- agno/reasoning/helpers.py +6 -7
- agno/reasoning/manager.py +1238 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +115 -1
- agno/reasoning/vertexai.py +85 -1
- agno/registry/__init__.py +3 -0
- agno/registry/registry.py +68 -0
- agno/remote/__init__.py +3 -0
- agno/remote/base.py +581 -0
- agno/run/__init__.py +2 -4
- agno/run/agent.py +134 -19
- agno/run/base.py +49 -1
- agno/run/cancel.py +65 -52
- agno/run/cancellation_management/__init__.py +9 -0
- agno/run/cancellation_management/base.py +78 -0
- agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
- agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +111 -19
- agno/run/workflow.py +2 -1
- agno/session/agent.py +57 -92
- agno/session/summary.py +1 -1
- agno/session/team.py +62 -115
- agno/session/workflow.py +353 -57
- agno/skills/__init__.py +17 -0
- agno/skills/agent_skills.py +377 -0
- agno/skills/errors.py +32 -0
- agno/skills/loaders/__init__.py +4 -0
- agno/skills/loaders/base.py +27 -0
- agno/skills/loaders/local.py +216 -0
- agno/skills/skill.py +65 -0
- agno/skills/utils.py +107 -0
- agno/skills/validator.py +277 -0
- agno/table.py +10 -0
- agno/team/__init__.py +5 -1
- agno/team/remote.py +447 -0
- agno/team/team.py +3769 -2202
- agno/tools/brandfetch.py +27 -18
- agno/tools/browserbase.py +225 -16
- agno/tools/crawl4ai.py +3 -0
- agno/tools/duckduckgo.py +25 -71
- agno/tools/exa.py +0 -21
- agno/tools/file.py +14 -13
- agno/tools/file_generation.py +12 -6
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +94 -113
- agno/tools/google_bigquery.py +11 -2
- agno/tools/google_drive.py +4 -3
- agno/tools/knowledge.py +9 -4
- agno/tools/mcp/mcp.py +301 -18
- agno/tools/mcp/multi_mcp.py +269 -14
- agno/tools/mem0.py +11 -10
- agno/tools/memory.py +47 -46
- agno/tools/mlx_transcribe.py +10 -7
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/parallel.py +0 -7
- agno/tools/postgres.py +76 -36
- agno/tools/python.py +14 -6
- agno/tools/reasoning.py +30 -23
- agno/tools/redshift.py +406 -0
- agno/tools/shopify.py +1519 -0
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +4 -1
- agno/tools/toolkit.py +253 -18
- agno/tools/websearch.py +93 -0
- agno/tools/website.py +1 -1
- agno/tools/wikipedia.py +1 -1
- agno/tools/workflow.py +56 -48
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +161 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +112 -0
- agno/utils/agent.py +251 -10
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +264 -7
- agno/utils/hooks.py +111 -3
- agno/utils/http.py +161 -2
- agno/utils/mcp.py +49 -8
- agno/utils/media.py +22 -1
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +20 -5
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/os.py +0 -0
- agno/utils/print_response/agent.py +99 -16
- agno/utils/print_response/team.py +223 -24
- agno/utils/print_response/workflow.py +0 -2
- agno/utils/prompts.py +8 -6
- agno/utils/remote.py +23 -0
- agno/utils/response.py +1 -13
- agno/utils/string.py +91 -2
- agno/utils/team.py +62 -12
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +15 -2
- agno/vectordb/cassandra/cassandra.py +1 -1
- agno/vectordb/chroma/__init__.py +2 -1
- agno/vectordb/chroma/chromadb.py +468 -23
- agno/vectordb/clickhouse/clickhousedb.py +1 -1
- agno/vectordb/couchbase/couchbase.py +6 -2
- agno/vectordb/lancedb/lance_db.py +7 -38
- agno/vectordb/lightrag/lightrag.py +7 -6
- agno/vectordb/milvus/milvus.py +118 -84
- agno/vectordb/mongodb/__init__.py +2 -1
- agno/vectordb/mongodb/mongodb.py +14 -31
- agno/vectordb/pgvector/pgvector.py +120 -66
- agno/vectordb/pineconedb/pineconedb.py +2 -19
- agno/vectordb/qdrant/__init__.py +2 -1
- agno/vectordb/qdrant/qdrant.py +33 -56
- agno/vectordb/redis/__init__.py +2 -1
- agno/vectordb/redis/redisdb.py +19 -31
- agno/vectordb/singlestore/singlestore.py +17 -9
- agno/vectordb/surrealdb/surrealdb.py +2 -38
- agno/vectordb/weaviate/__init__.py +2 -1
- agno/vectordb/weaviate/weaviate.py +7 -3
- agno/workflow/__init__.py +5 -1
- agno/workflow/agent.py +2 -2
- agno/workflow/condition.py +12 -10
- agno/workflow/loop.py +28 -9
- agno/workflow/parallel.py +21 -13
- agno/workflow/remote.py +362 -0
- agno/workflow/router.py +12 -9
- agno/workflow/step.py +261 -36
- agno/workflow/steps.py +12 -8
- agno/workflow/types.py +40 -77
- agno/workflow/workflow.py +939 -213
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
- agno-2.4.3.dist-info/RECORD +677 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
- agno/tools/googlesearch.py +0 -98
- agno/tools/memori.py +0 -339
- agno-2.2.13.dist-info/RECORD +0 -575
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/top_level.txt +0 -0
agno/learn/schemas.py
ADDED
|
@@ -0,0 +1,1114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LearningMachine Schemas
|
|
3
|
+
=======================
|
|
4
|
+
Dataclasses for each learning type.
|
|
5
|
+
|
|
6
|
+
Uses pure dataclasses to avoid runtime overhead.
|
|
7
|
+
All parsing is done via from_dict() which never raises.
|
|
8
|
+
|
|
9
|
+
Classes are designed to be extended - from_dict() and to_dict()
|
|
10
|
+
automatically handle subclass fields via dataclasses.fields().
|
|
11
|
+
|
|
12
|
+
Field Descriptions
|
|
13
|
+
When extending schemas, use field metadata to provide descriptions
|
|
14
|
+
that will be shown to the LLM:
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class MyUserProfile(UserProfile):
|
|
18
|
+
company: Optional[str] = field(
|
|
19
|
+
default=None,
|
|
20
|
+
metadata={"description": "Where they work"}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
The LLM will see this description when deciding how to update fields.
|
|
24
|
+
|
|
25
|
+
Schemas:
|
|
26
|
+
- UserProfile: Long-term user memory
|
|
27
|
+
- SessionContext: Current session state
|
|
28
|
+
- LearnedKnowledge: Reusable knowledge/insights
|
|
29
|
+
- EntityMemory: Third-party entity facts
|
|
30
|
+
- Decision: Decision logs (Phase 2)
|
|
31
|
+
- Feedback: Behavioral feedback (Phase 2)
|
|
32
|
+
- InstructionUpdate: Self-improvement (Phase 3)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from dataclasses import asdict, dataclass, field, fields
|
|
36
|
+
from typing import Any, Dict, List, Optional
|
|
37
|
+
|
|
38
|
+
from agno.learn.utils import _parse_json, _safe_get
|
|
39
|
+
from agno.utils.log import log_debug
|
|
40
|
+
|
|
41
|
+
# =============================================================================
|
|
42
|
+
# Helper for debug logging
|
|
43
|
+
# =============================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _truncate_for_log(data: Any, max_len: int = 100) -> str:
|
|
47
|
+
"""Truncate data for logging to avoid massive log entries."""
|
|
48
|
+
s = str(data)
|
|
49
|
+
if len(s) > max_len:
|
|
50
|
+
return s[:max_len] + "..."
|
|
51
|
+
return s
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# =============================================================================
|
|
55
|
+
# User Profile Schema
|
|
56
|
+
# =============================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class UserProfile:
|
|
61
|
+
"""Schema for User Profile learning type.
|
|
62
|
+
|
|
63
|
+
Captures long-term structured profile information about a user that persists
|
|
64
|
+
across sessions. Designed to be extended with custom fields.
|
|
65
|
+
|
|
66
|
+
## Extending with Custom Fields
|
|
67
|
+
|
|
68
|
+
Use field metadata to provide descriptions for the LLM:
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class MyUserProfile(UserProfile):
|
|
72
|
+
company: Optional[str] = field(
|
|
73
|
+
default=None,
|
|
74
|
+
metadata={"description": "Company or organization they work for"}
|
|
75
|
+
)
|
|
76
|
+
role: Optional[str] = field(
|
|
77
|
+
default=None,
|
|
78
|
+
metadata={"description": "Job title or role"}
|
|
79
|
+
)
|
|
80
|
+
timezone: Optional[str] = field(
|
|
81
|
+
default=None,
|
|
82
|
+
metadata={"description": "User's timezone (e.g., America/New_York)"}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
Attributes:
|
|
86
|
+
user_id: Required unique identifier for the user.
|
|
87
|
+
name: User's full name.
|
|
88
|
+
preferred_name: How they prefer to be addressed (nickname, first name, etc).
|
|
89
|
+
agent_id: Which agent created this profile.
|
|
90
|
+
team_id: Which team created this profile.
|
|
91
|
+
created_at: When the profile was created (ISO format).
|
|
92
|
+
updated_at: When the profile was last updated (ISO format).
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
user_id: str
|
|
96
|
+
name: Optional[str] = field(default=None, metadata={"description": "User's full name"})
|
|
97
|
+
preferred_name: Optional[str] = field(
|
|
98
|
+
default=None, metadata={"description": "How they prefer to be addressed (nickname, first name, etc)"}
|
|
99
|
+
)
|
|
100
|
+
agent_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
101
|
+
team_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
102
|
+
created_at: Optional[str] = field(default=None, metadata={"internal": True})
|
|
103
|
+
updated_at: Optional[str] = field(default=None, metadata={"internal": True})
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_dict(cls, data: Any) -> Optional["UserProfile"]:
|
|
107
|
+
"""Parse from dict/JSON, returning None on any failure.
|
|
108
|
+
|
|
109
|
+
Works with subclasses - automatically handles additional fields.
|
|
110
|
+
"""
|
|
111
|
+
if data is None:
|
|
112
|
+
return None
|
|
113
|
+
if isinstance(data, cls):
|
|
114
|
+
return data
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
parsed = _parse_json(data)
|
|
118
|
+
if not parsed:
|
|
119
|
+
log_debug(f"{cls.__name__}.from_dict: _parse_json returned None for data={_truncate_for_log(data)}")
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
# user_id is required
|
|
123
|
+
if not parsed.get("user_id"):
|
|
124
|
+
log_debug(f"{cls.__name__}.from_dict: missing required field 'user_id'")
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
# Get field names for this class (includes subclass fields)
|
|
128
|
+
field_names = {f.name for f in fields(cls)}
|
|
129
|
+
kwargs = {k: v for k, v in parsed.items() if k in field_names}
|
|
130
|
+
|
|
131
|
+
return cls(**kwargs)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
log_debug(f"{cls.__name__}.from_dict failed: {e}, data={_truncate_for_log(data)}")
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
137
|
+
"""Convert to dict. Works with subclasses."""
|
|
138
|
+
try:
|
|
139
|
+
return asdict(self)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
log_debug(f"{self.__class__.__name__}.to_dict failed: {e}")
|
|
142
|
+
return {}
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def get_updateable_fields(cls) -> Dict[str, Dict[str, Any]]:
|
|
146
|
+
"""Get fields that can be updated via update_profile tool.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Dict mapping field name to field info including description.
|
|
150
|
+
Excludes internal fields (user_id, timestamps, etc).
|
|
151
|
+
"""
|
|
152
|
+
skip = {"user_id", "created_at", "updated_at", "agent_id", "team_id"}
|
|
153
|
+
|
|
154
|
+
result = {}
|
|
155
|
+
for f in fields(cls):
|
|
156
|
+
if f.name in skip:
|
|
157
|
+
continue
|
|
158
|
+
# Skip fields marked as internal
|
|
159
|
+
if f.metadata.get("internal"):
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
result[f.name] = {
|
|
163
|
+
"type": f.type,
|
|
164
|
+
"description": f.metadata.get("description", f"User's {f.name.replace('_', ' ')}"),
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
return f"UserProfile(user_id={self.user_id})"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class Memories:
|
|
175
|
+
"""Schema for Memories learning type.
|
|
176
|
+
|
|
177
|
+
Captures unstructured observations about a user that don't fit
|
|
178
|
+
into structured profile fields. These are long-term memories
|
|
179
|
+
that persist across sessions.
|
|
180
|
+
|
|
181
|
+
Attributes:
|
|
182
|
+
user_id: Required unique identifier for the user.
|
|
183
|
+
memories: List of memory entries, each with 'id' and 'content'.
|
|
184
|
+
agent_id: Which agent created these memories.
|
|
185
|
+
team_id: Which team created these memories.
|
|
186
|
+
created_at: When the memories were created (ISO format).
|
|
187
|
+
updated_at: When the memories were last updated (ISO format).
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
user_id: str
|
|
191
|
+
memories: List[Dict[str, Any]] = field(default_factory=list)
|
|
192
|
+
agent_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
193
|
+
team_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
194
|
+
created_at: Optional[str] = field(default=None, metadata={"internal": True})
|
|
195
|
+
updated_at: Optional[str] = field(default=None, metadata={"internal": True})
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def from_dict(cls, data: Any) -> Optional["Memories"]:
|
|
199
|
+
"""Parse from dict/JSON, returning None on any failure.
|
|
200
|
+
|
|
201
|
+
Works with subclasses - automatically handles additional fields.
|
|
202
|
+
"""
|
|
203
|
+
if data is None:
|
|
204
|
+
return None
|
|
205
|
+
if isinstance(data, cls):
|
|
206
|
+
return data
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
parsed = _parse_json(data)
|
|
210
|
+
if not parsed:
|
|
211
|
+
log_debug(f"{cls.__name__}.from_dict: _parse_json returned None for data={_truncate_for_log(data)}")
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
# user_id is required
|
|
215
|
+
if not parsed.get("user_id"):
|
|
216
|
+
log_debug(f"{cls.__name__}.from_dict: missing required field 'user_id'")
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
# Get field names for this class (includes subclass fields)
|
|
220
|
+
field_names = {f.name for f in fields(cls)}
|
|
221
|
+
kwargs = {k: v for k, v in parsed.items() if k in field_names}
|
|
222
|
+
|
|
223
|
+
return cls(**kwargs)
|
|
224
|
+
except Exception as e:
|
|
225
|
+
log_debug(f"{cls.__name__}.from_dict failed: {e}, data={_truncate_for_log(data)}")
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
229
|
+
"""Convert to dict. Works with subclasses."""
|
|
230
|
+
try:
|
|
231
|
+
return asdict(self)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
log_debug(f"{self.__class__.__name__}.to_dict failed: {e}")
|
|
234
|
+
return {}
|
|
235
|
+
|
|
236
|
+
def add_memory(self, content: str, **kwargs) -> str:
|
|
237
|
+
"""Add a new memory.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
content: The memory text to add.
|
|
241
|
+
**kwargs: Additional fields (source, timestamp, etc.)
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
The generated memory ID.
|
|
245
|
+
"""
|
|
246
|
+
import uuid
|
|
247
|
+
|
|
248
|
+
memory_id = str(uuid.uuid4())[:8]
|
|
249
|
+
|
|
250
|
+
if content and content.strip():
|
|
251
|
+
self.memories.append({"id": memory_id, "content": content.strip(), **kwargs})
|
|
252
|
+
|
|
253
|
+
return memory_id
|
|
254
|
+
|
|
255
|
+
def get_memory(self, memory_id: str) -> Optional[Dict[str, Any]]:
|
|
256
|
+
"""Get a specific memory by ID."""
|
|
257
|
+
for mem in self.memories:
|
|
258
|
+
if isinstance(mem, dict) and mem.get("id") == memory_id:
|
|
259
|
+
return mem
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
def update_memory(self, memory_id: str, content: str, **kwargs) -> bool:
|
|
263
|
+
"""Update an existing memory.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
True if memory was found and updated, False otherwise.
|
|
267
|
+
"""
|
|
268
|
+
for mem in self.memories:
|
|
269
|
+
if isinstance(mem, dict) and mem.get("id") == memory_id:
|
|
270
|
+
mem["content"] = content.strip()
|
|
271
|
+
mem.update(kwargs)
|
|
272
|
+
return True
|
|
273
|
+
return False
|
|
274
|
+
|
|
275
|
+
def delete_memory(self, memory_id: str) -> bool:
|
|
276
|
+
"""Delete a memory by ID.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
True if memory was found and deleted, False otherwise.
|
|
280
|
+
"""
|
|
281
|
+
original_len = len(self.memories)
|
|
282
|
+
self.memories = [mem for mem in self.memories if not (isinstance(mem, dict) and mem.get("id") == memory_id)]
|
|
283
|
+
return len(self.memories) < original_len
|
|
284
|
+
|
|
285
|
+
def get_memories_text(self) -> str:
|
|
286
|
+
"""Get all memories as a formatted string for prompts."""
|
|
287
|
+
if not self.memories:
|
|
288
|
+
return ""
|
|
289
|
+
|
|
290
|
+
lines = []
|
|
291
|
+
for m in self.memories:
|
|
292
|
+
content = m.get("content") if isinstance(m, dict) else str(m)
|
|
293
|
+
if content:
|
|
294
|
+
lines.append(f"- {content}")
|
|
295
|
+
|
|
296
|
+
return "\n".join(lines)
|
|
297
|
+
|
|
298
|
+
def __repr__(self) -> str:
|
|
299
|
+
return f"Memories(user_id={self.user_id})"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# =============================================================================
|
|
303
|
+
# Session Context Schema
|
|
304
|
+
# =============================================================================
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@dataclass
|
|
308
|
+
class SessionContext:
|
|
309
|
+
"""Schema for Session Context learning type.
|
|
310
|
+
|
|
311
|
+
Captures state and summary for the current session.
|
|
312
|
+
Unlike UserProfile which accumulates, this is REPLACED on each update.
|
|
313
|
+
|
|
314
|
+
Key behavior: Extraction receives the previous context and updates it,
|
|
315
|
+
ensuring continuity even when message history is truncated.
|
|
316
|
+
|
|
317
|
+
Attributes:
|
|
318
|
+
session_id: Required unique identifier for the session.
|
|
319
|
+
user_id: Which user this session belongs to.
|
|
320
|
+
summary: What's happened in this session.
|
|
321
|
+
goal: What the user is trying to accomplish.
|
|
322
|
+
plan: Steps to achieve the goal.
|
|
323
|
+
progress: Which steps have been completed.
|
|
324
|
+
agent_id: Which agent is running this session.
|
|
325
|
+
team_id: Which team is running this session.
|
|
326
|
+
created_at: When the session started (ISO format).
|
|
327
|
+
updated_at: When the context was last updated (ISO format).
|
|
328
|
+
|
|
329
|
+
Example - Extending with custom fields:
|
|
330
|
+
@dataclass
|
|
331
|
+
class MySessionContext(SessionContext):
|
|
332
|
+
mood: Optional[str] = field(
|
|
333
|
+
default=None,
|
|
334
|
+
metadata={"description": "User's current mood or emotional state"}
|
|
335
|
+
)
|
|
336
|
+
blockers: List[str] = field(
|
|
337
|
+
default_factory=list,
|
|
338
|
+
metadata={"description": "Current blockers or obstacles"}
|
|
339
|
+
)
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
session_id: str
|
|
343
|
+
user_id: Optional[str] = None
|
|
344
|
+
summary: Optional[str] = field(
|
|
345
|
+
default=None, metadata={"description": "Summary of what's been discussed in this session"}
|
|
346
|
+
)
|
|
347
|
+
goal: Optional[str] = field(default=None, metadata={"description": "What the user is trying to accomplish"})
|
|
348
|
+
plan: Optional[List[str]] = field(default=None, metadata={"description": "Steps to achieve the goal"})
|
|
349
|
+
progress: Optional[List[str]] = field(default=None, metadata={"description": "Which steps have been completed"})
|
|
350
|
+
agent_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
351
|
+
team_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
352
|
+
created_at: Optional[str] = field(default=None, metadata={"internal": True})
|
|
353
|
+
updated_at: Optional[str] = field(default=None, metadata={"internal": True})
|
|
354
|
+
|
|
355
|
+
@classmethod
|
|
356
|
+
def from_dict(cls, data: Any) -> Optional["SessionContext"]:
|
|
357
|
+
"""Parse from dict/JSON, returning None on any failure."""
|
|
358
|
+
if data is None:
|
|
359
|
+
return None
|
|
360
|
+
if isinstance(data, cls):
|
|
361
|
+
return data
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
parsed = _parse_json(data)
|
|
365
|
+
if not parsed:
|
|
366
|
+
log_debug(f"{cls.__name__}.from_dict: _parse_json returned None for data={_truncate_for_log(data)}")
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
# session_id is required
|
|
370
|
+
if not parsed.get("session_id"):
|
|
371
|
+
log_debug(f"{cls.__name__}.from_dict: missing required field 'session_id'")
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
field_names = {f.name for f in fields(cls)}
|
|
375
|
+
kwargs = {k: v for k, v in parsed.items() if k in field_names}
|
|
376
|
+
|
|
377
|
+
return cls(**kwargs)
|
|
378
|
+
except Exception as e:
|
|
379
|
+
log_debug(f"{cls.__name__}.from_dict failed: {e}, data={_truncate_for_log(data)}")
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
383
|
+
"""Convert to dict."""
|
|
384
|
+
try:
|
|
385
|
+
return asdict(self)
|
|
386
|
+
except Exception as e:
|
|
387
|
+
log_debug(f"{self.__class__.__name__}.to_dict failed: {e}")
|
|
388
|
+
return {}
|
|
389
|
+
|
|
390
|
+
def get_context_text(self) -> str:
|
|
391
|
+
"""Get session context as a formatted string for prompts."""
|
|
392
|
+
parts = []
|
|
393
|
+
|
|
394
|
+
if self.summary:
|
|
395
|
+
parts.append(f"Summary: {self.summary}")
|
|
396
|
+
|
|
397
|
+
if self.goal:
|
|
398
|
+
parts.append(f"Goal: {self.goal}")
|
|
399
|
+
|
|
400
|
+
if self.plan:
|
|
401
|
+
plan_text = "\n".join(f" {i + 1}. {step}" for i, step in enumerate(self.plan))
|
|
402
|
+
parts.append(f"Plan:\n{plan_text}")
|
|
403
|
+
|
|
404
|
+
if self.progress:
|
|
405
|
+
progress_text = "\n".join(f" ✓ {step}" for step in self.progress)
|
|
406
|
+
parts.append(f"Completed:\n{progress_text}")
|
|
407
|
+
|
|
408
|
+
return "\n\n".join(parts)
|
|
409
|
+
|
|
410
|
+
def __repr__(self) -> str:
|
|
411
|
+
return f"SessionContext(session_id={self.session_id})"
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# =============================================================================
|
|
415
|
+
# Learned Knowledge Schema
|
|
416
|
+
# =============================================================================
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@dataclass
|
|
420
|
+
class LearnedKnowledge:
|
|
421
|
+
"""Schema for Learned Knowledge learning type.
|
|
422
|
+
|
|
423
|
+
Captures reusable insights that apply across users and agents.
|
|
424
|
+
|
|
425
|
+
- title: Short, descriptive title for the learning.
|
|
426
|
+
- learning: The actual insight or pattern.
|
|
427
|
+
- context: When/where this learning applies.
|
|
428
|
+
- tags: Categories for organization.
|
|
429
|
+
- namespace: Sharing boundary for this learning.
|
|
430
|
+
|
|
431
|
+
Example:
|
|
432
|
+
LearnedKnowledge(
|
|
433
|
+
title="Python async best practices",
|
|
434
|
+
learning="Always use asyncio.gather() for concurrent I/O tasks",
|
|
435
|
+
context="When optimizing I/O-bound Python applications",
|
|
436
|
+
tags=["python", "async", "performance"]
|
|
437
|
+
)
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
title: str
|
|
441
|
+
learning: str
|
|
442
|
+
context: Optional[str] = None
|
|
443
|
+
tags: Optional[List[str]] = None
|
|
444
|
+
user_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
445
|
+
namespace: Optional[str] = field(default=None, metadata={"internal": True})
|
|
446
|
+
agent_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
447
|
+
team_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
448
|
+
created_at: Optional[str] = field(default=None, metadata={"internal": True})
|
|
449
|
+
updated_at: Optional[str] = field(default=None, metadata={"internal": True})
|
|
450
|
+
|
|
451
|
+
@classmethod
|
|
452
|
+
def from_dict(cls, data: Any) -> Optional["LearnedKnowledge"]:
|
|
453
|
+
"""Parse from dict/JSON, returning None on any failure."""
|
|
454
|
+
if data is None:
|
|
455
|
+
return None
|
|
456
|
+
if isinstance(data, cls):
|
|
457
|
+
return data
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
parsed = _parse_json(data)
|
|
461
|
+
if not parsed:
|
|
462
|
+
log_debug(f"{cls.__name__}.from_dict: _parse_json returned None for data={_truncate_for_log(data)}")
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
# title and learning are required
|
|
466
|
+
if not parsed.get("title") or not parsed.get("learning"):
|
|
467
|
+
log_debug(f"{cls.__name__}.from_dict: missing required fields 'title' or 'learning'")
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
field_names = {f.name for f in fields(cls)}
|
|
471
|
+
kwargs = {k: v for k, v in parsed.items() if k in field_names}
|
|
472
|
+
|
|
473
|
+
return cls(**kwargs)
|
|
474
|
+
except Exception as e:
|
|
475
|
+
log_debug(f"{cls.__name__}.from_dict failed: {e}, data={_truncate_for_log(data)}")
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
479
|
+
"""Convert to dict."""
|
|
480
|
+
try:
|
|
481
|
+
return asdict(self)
|
|
482
|
+
except Exception as e:
|
|
483
|
+
log_debug(f"{self.__class__.__name__}.to_dict failed: {e}")
|
|
484
|
+
return {}
|
|
485
|
+
|
|
486
|
+
def to_text(self) -> str:
|
|
487
|
+
"""Convert learning to searchable text format for vector storage."""
|
|
488
|
+
parts = [f"Title: {self.title}", f"Learning: {self.learning}"]
|
|
489
|
+
if self.context:
|
|
490
|
+
parts.append(f"Context: {self.context}")
|
|
491
|
+
if self.tags:
|
|
492
|
+
parts.append(f"Tags: {', '.join(self.tags)}")
|
|
493
|
+
return "\n".join(parts)
|
|
494
|
+
|
|
495
|
+
def __repr__(self) -> str:
|
|
496
|
+
return f"LearnedKnowledge(title={self.title})"
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# =============================================================================
|
|
500
|
+
# Entity Memory Schema
|
|
501
|
+
# =============================================================================
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
@dataclass
|
|
505
|
+
class EntityMemory:
|
|
506
|
+
"""Schema for Entity Memory learning type.
|
|
507
|
+
|
|
508
|
+
Captures facts about third-party entities: companies, projects,
|
|
509
|
+
people, systems, products. Like UserProfile but for non-users.
|
|
510
|
+
|
|
511
|
+
Structure:
|
|
512
|
+
- **Core**: name, description, properties (key-value pairs)
|
|
513
|
+
- **Facts**: Semantic memory ("Acme uses PostgreSQL")
|
|
514
|
+
- **Events**: Episodic memory ("Acme launched v2 on Jan 15")
|
|
515
|
+
- **Relationships**: Graph edges ("Bob is CEO of Acme")
|
|
516
|
+
|
|
517
|
+
Common Entity Types:
|
|
518
|
+
- "company", "project", "person", "system", "product"
|
|
519
|
+
- Any string is valid.
|
|
520
|
+
|
|
521
|
+
Example:
|
|
522
|
+
EntityMemory(
|
|
523
|
+
entity_id="acme_corp",
|
|
524
|
+
entity_type="company",
|
|
525
|
+
name="Acme Corporation",
|
|
526
|
+
description="Enterprise software company",
|
|
527
|
+
properties={"industry": "fintech", "size": "startup"},
|
|
528
|
+
facts=[
|
|
529
|
+
{"id": "f1", "content": "Uses PostgreSQL for main database"},
|
|
530
|
+
{"id": "f2", "content": "API uses OAuth2 authentication"},
|
|
531
|
+
],
|
|
532
|
+
events=[
|
|
533
|
+
{"id": "e1", "content": "Launched v2.0", "date": "2024-01-15"},
|
|
534
|
+
],
|
|
535
|
+
relationships=[
|
|
536
|
+
{"entity_id": "bob_smith", "relation": "CEO"},
|
|
537
|
+
],
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
Attributes:
|
|
541
|
+
entity_id: Unique identifier (lowercase, underscores: "acme_corp").
|
|
542
|
+
entity_type: Type of entity ("company", "project", "person", etc).
|
|
543
|
+
name: Display name for the entity.
|
|
544
|
+
description: Brief description of what this entity is.
|
|
545
|
+
properties: Key-value properties (industry, tech_stack, etc).
|
|
546
|
+
facts: Semantic memories - timeless facts about the entity.
|
|
547
|
+
events: Episodic memories - time-bound occurrences.
|
|
548
|
+
relationships: Connections to other entities.
|
|
549
|
+
namespace: Sharing boundary for this entity.
|
|
550
|
+
user_id: Owner user (if namespace="user").
|
|
551
|
+
agent_id: Which agent created this.
|
|
552
|
+
team_id: Which team context.
|
|
553
|
+
created_at: When first created.
|
|
554
|
+
updated_at: When last modified.
|
|
555
|
+
"""
|
|
556
|
+
|
|
557
|
+
entity_id: str
|
|
558
|
+
entity_type: str = field(metadata={"description": "Type: company, project, person, system, product, etc"})
|
|
559
|
+
|
|
560
|
+
# Core properties
|
|
561
|
+
name: Optional[str] = field(default=None, metadata={"description": "Display name for the entity"})
|
|
562
|
+
description: Optional[str] = field(
|
|
563
|
+
default=None, metadata={"description": "Brief description of what this entity is"}
|
|
564
|
+
)
|
|
565
|
+
properties: Dict[str, str] = field(
|
|
566
|
+
default_factory=dict, metadata={"description": "Key-value properties (industry, tech_stack, etc)"}
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Semantic memory (facts)
|
|
570
|
+
facts: List[Dict[str, Any]] = field(default_factory=list)
|
|
571
|
+
# [{"id": "abc", "content": "Uses PostgreSQL", "confidence": 0.9, "source": "..."}]
|
|
572
|
+
|
|
573
|
+
# Episodic memory (events)
|
|
574
|
+
events: List[Dict[str, Any]] = field(default_factory=list)
|
|
575
|
+
# [{"id": "xyz", "content": "Had outage on 2024-01-15", "date": "2024-01-15"}]
|
|
576
|
+
|
|
577
|
+
# Relationships (graph edges)
|
|
578
|
+
relationships: List[Dict[str, Any]] = field(default_factory=list)
|
|
579
|
+
# [{"entity_id": "bob", "relation": "CEO", "direction": "incoming"}]
|
|
580
|
+
|
|
581
|
+
# Scope
|
|
582
|
+
namespace: Optional[str] = field(default=None, metadata={"internal": True})
|
|
583
|
+
user_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
584
|
+
agent_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
585
|
+
team_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
586
|
+
created_at: Optional[str] = field(default=None, metadata={"internal": True})
|
|
587
|
+
updated_at: Optional[str] = field(default=None, metadata={"internal": True})
|
|
588
|
+
|
|
589
|
+
@classmethod
|
|
590
|
+
def from_dict(cls, data: Any) -> Optional["EntityMemory"]:
|
|
591
|
+
"""Parse from dict/JSON, returning None on any failure."""
|
|
592
|
+
if data is None:
|
|
593
|
+
return None
|
|
594
|
+
if isinstance(data, cls):
|
|
595
|
+
return data
|
|
596
|
+
|
|
597
|
+
try:
|
|
598
|
+
parsed = _parse_json(data)
|
|
599
|
+
if not parsed:
|
|
600
|
+
log_debug(f"{cls.__name__}.from_dict: _parse_json returned None for data={_truncate_for_log(data)}")
|
|
601
|
+
return None
|
|
602
|
+
|
|
603
|
+
# entity_id and entity_type are required
|
|
604
|
+
if not parsed.get("entity_id") or not parsed.get("entity_type"):
|
|
605
|
+
log_debug(f"{cls.__name__}.from_dict: missing required fields 'entity_id' or 'entity_type'")
|
|
606
|
+
return None
|
|
607
|
+
|
|
608
|
+
field_names = {f.name for f in fields(cls)}
|
|
609
|
+
kwargs = {k: v for k, v in parsed.items() if k in field_names}
|
|
610
|
+
|
|
611
|
+
return cls(**kwargs)
|
|
612
|
+
except Exception as e:
|
|
613
|
+
log_debug(f"{cls.__name__}.from_dict failed: {e}, data={_truncate_for_log(data)}")
|
|
614
|
+
return None
|
|
615
|
+
|
|
616
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
617
|
+
"""Convert to dict."""
|
|
618
|
+
try:
|
|
619
|
+
return asdict(self)
|
|
620
|
+
except Exception as e:
|
|
621
|
+
log_debug(f"{self.__class__.__name__}.to_dict failed: {e}")
|
|
622
|
+
return {}
|
|
623
|
+
|
|
624
|
+
def add_fact(self, content: str, **kwargs) -> str:
|
|
625
|
+
"""Add a new fact to the entity.
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
content: The fact text.
|
|
629
|
+
**kwargs: Additional fields (confidence, source, etc).
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
The generated fact ID.
|
|
633
|
+
"""
|
|
634
|
+
import uuid
|
|
635
|
+
|
|
636
|
+
fact_id = str(uuid.uuid4())[:8]
|
|
637
|
+
|
|
638
|
+
if content and content.strip():
|
|
639
|
+
self.facts.append({"id": fact_id, "content": content.strip(), **kwargs})
|
|
640
|
+
|
|
641
|
+
return fact_id
|
|
642
|
+
|
|
643
|
+
def add_event(self, content: str, date: Optional[str] = None, **kwargs) -> str:
|
|
644
|
+
"""Add a new event to the entity.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
content: The event description.
|
|
648
|
+
date: When the event occurred (ISO format or natural language).
|
|
649
|
+
**kwargs: Additional fields.
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
The generated event ID.
|
|
653
|
+
"""
|
|
654
|
+
import uuid
|
|
655
|
+
|
|
656
|
+
event_id = str(uuid.uuid4())[:8]
|
|
657
|
+
|
|
658
|
+
if content and content.strip():
|
|
659
|
+
event = {"id": event_id, "content": content.strip(), **kwargs}
|
|
660
|
+
if date:
|
|
661
|
+
event["date"] = date
|
|
662
|
+
self.events.append(event)
|
|
663
|
+
|
|
664
|
+
return event_id
|
|
665
|
+
|
|
666
|
+
def add_relationship(self, related_entity_id: str, relation: str, direction: str = "outgoing", **kwargs) -> str:
|
|
667
|
+
"""Add a relationship to another entity.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
related_entity_id: The other entity's ID.
|
|
671
|
+
relation: The relationship type ("CEO", "owns", "part_of", etc).
|
|
672
|
+
direction: "outgoing" (this → other) or "incoming" (other → this).
|
|
673
|
+
**kwargs: Additional fields.
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
The generated relationship ID.
|
|
677
|
+
"""
|
|
678
|
+
import uuid
|
|
679
|
+
|
|
680
|
+
rel_id = str(uuid.uuid4())[:8]
|
|
681
|
+
|
|
682
|
+
self.relationships.append(
|
|
683
|
+
{"id": rel_id, "entity_id": related_entity_id, "relation": relation, "direction": direction, **kwargs}
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
return rel_id
|
|
687
|
+
|
|
688
|
+
def get_fact(self, fact_id: str) -> Optional[Dict[str, Any]]:
|
|
689
|
+
"""Get a specific fact by ID."""
|
|
690
|
+
for fact in self.facts:
|
|
691
|
+
if isinstance(fact, dict) and fact.get("id") == fact_id:
|
|
692
|
+
return fact
|
|
693
|
+
return None
|
|
694
|
+
|
|
695
|
+
def update_fact(self, fact_id: str, content: str, **kwargs) -> bool:
|
|
696
|
+
"""Update an existing fact.
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
True if fact was found and updated, False otherwise.
|
|
700
|
+
"""
|
|
701
|
+
for fact in self.facts:
|
|
702
|
+
if isinstance(fact, dict) and fact.get("id") == fact_id:
|
|
703
|
+
fact["content"] = content.strip()
|
|
704
|
+
fact.update(kwargs)
|
|
705
|
+
return True
|
|
706
|
+
return False
|
|
707
|
+
|
|
708
|
+
def delete_fact(self, fact_id: str) -> bool:
|
|
709
|
+
"""Delete a fact by ID.
|
|
710
|
+
|
|
711
|
+
Returns:
|
|
712
|
+
True if fact was found and deleted, False otherwise.
|
|
713
|
+
"""
|
|
714
|
+
original_len = len(self.facts)
|
|
715
|
+
self.facts = [f for f in self.facts if not (isinstance(f, dict) and f.get("id") == fact_id)]
|
|
716
|
+
return len(self.facts) < original_len
|
|
717
|
+
|
|
718
|
+
def get_context_text(self) -> str:
|
|
719
|
+
"""Get entity as formatted string for prompts."""
|
|
720
|
+
parts = []
|
|
721
|
+
|
|
722
|
+
if self.name:
|
|
723
|
+
parts.append(f"**{self.name}** ({self.entity_type})")
|
|
724
|
+
else:
|
|
725
|
+
parts.append(f"**{self.entity_id}** ({self.entity_type})")
|
|
726
|
+
|
|
727
|
+
if self.description:
|
|
728
|
+
parts.append(self.description)
|
|
729
|
+
|
|
730
|
+
if self.properties:
|
|
731
|
+
props = ", ".join(f"{k}: {v}" for k, v in self.properties.items())
|
|
732
|
+
parts.append(f"Properties: {props}")
|
|
733
|
+
|
|
734
|
+
if self.facts:
|
|
735
|
+
facts_text = "\n".join(f" - {f.get('content', f)}" for f in self.facts)
|
|
736
|
+
parts.append(f"Facts:\n{facts_text}")
|
|
737
|
+
|
|
738
|
+
if self.events:
|
|
739
|
+
events_text = "\n".join(
|
|
740
|
+
f" - {e.get('content', e)}" + (f" ({e.get('date')})" if e.get("date") else "") for e in self.events
|
|
741
|
+
)
|
|
742
|
+
parts.append(f"Events:\n{events_text}")
|
|
743
|
+
|
|
744
|
+
if self.relationships:
|
|
745
|
+
rels_text = "\n".join(f" - {r.get('relation')}: {r.get('entity_id')}" for r in self.relationships)
|
|
746
|
+
parts.append(f"Relationships:\n{rels_text}")
|
|
747
|
+
|
|
748
|
+
return "\n\n".join(parts)
|
|
749
|
+
|
|
750
|
+
@classmethod
|
|
751
|
+
def get_updateable_fields(cls) -> Dict[str, Dict[str, Any]]:
|
|
752
|
+
"""Get fields that can be updated via update tools.
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
Dict mapping field name to field info including description.
|
|
756
|
+
Excludes internal fields and collections (facts, events, relationships).
|
|
757
|
+
"""
|
|
758
|
+
skip = {
|
|
759
|
+
"entity_id",
|
|
760
|
+
"entity_type",
|
|
761
|
+
"facts",
|
|
762
|
+
"events",
|
|
763
|
+
"relationships",
|
|
764
|
+
"namespace",
|
|
765
|
+
"user_id",
|
|
766
|
+
"agent_id",
|
|
767
|
+
"team_id",
|
|
768
|
+
"created_at",
|
|
769
|
+
"updated_at",
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
result = {}
|
|
773
|
+
for f in fields(cls):
|
|
774
|
+
if f.name in skip:
|
|
775
|
+
continue
|
|
776
|
+
if f.metadata.get("internal"):
|
|
777
|
+
continue
|
|
778
|
+
|
|
779
|
+
result[f.name] = {
|
|
780
|
+
"type": f.type,
|
|
781
|
+
"description": f.metadata.get("description", f"Entity's {f.name.replace('_', ' ')}"),
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return result
|
|
785
|
+
|
|
786
|
+
def __repr__(self) -> str:
|
|
787
|
+
return f"EntityMemory(entity_id={self.entity_id})"
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
# =============================================================================
|
|
791
|
+
# Extraction Response Models (internal use by stores)
|
|
792
|
+
# =============================================================================
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
@dataclass
|
|
796
|
+
class UserProfileExtractionResponse:
|
|
797
|
+
"""Response model for user profile extraction from LLM.
|
|
798
|
+
|
|
799
|
+
Used internally by UserProfileStore during background extraction.
|
|
800
|
+
"""
|
|
801
|
+
|
|
802
|
+
name: Optional[str] = None
|
|
803
|
+
preferred_name: Optional[str] = None
|
|
804
|
+
new_memories: List[str] = field(default_factory=list)
|
|
805
|
+
|
|
806
|
+
@classmethod
|
|
807
|
+
def from_dict(cls, data: Any) -> Optional["UserProfileExtractionResponse"]:
|
|
808
|
+
"""Parse from dict/JSON, returning None on any failure."""
|
|
809
|
+
if data is None:
|
|
810
|
+
return None
|
|
811
|
+
if isinstance(data, cls):
|
|
812
|
+
return data
|
|
813
|
+
|
|
814
|
+
try:
|
|
815
|
+
parsed = _parse_json(data)
|
|
816
|
+
if not parsed:
|
|
817
|
+
log_debug(f"{cls.__name__}.from_dict: _parse_json returned None for data={_truncate_for_log(data)}")
|
|
818
|
+
return None
|
|
819
|
+
|
|
820
|
+
return cls(
|
|
821
|
+
name=_safe_get(parsed, "name"),
|
|
822
|
+
preferred_name=_safe_get(parsed, "preferred_name"),
|
|
823
|
+
new_memories=_safe_get(parsed, "new_memories") or [],
|
|
824
|
+
)
|
|
825
|
+
except Exception as e:
|
|
826
|
+
log_debug(f"{cls.__name__}.from_dict failed: {e}, data={_truncate_for_log(data)}")
|
|
827
|
+
return None
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
@dataclass
|
|
831
|
+
class SessionSummaryExtractionResponse:
|
|
832
|
+
"""Response model for summary-only session extraction from LLM."""
|
|
833
|
+
|
|
834
|
+
summary: str = ""
|
|
835
|
+
|
|
836
|
+
@classmethod
|
|
837
|
+
def from_dict(cls, data: Any) -> Optional["SessionSummaryExtractionResponse"]:
|
|
838
|
+
"""Parse from dict/JSON, returning None on any failure."""
|
|
839
|
+
if data is None:
|
|
840
|
+
return None
|
|
841
|
+
if isinstance(data, cls):
|
|
842
|
+
return data
|
|
843
|
+
|
|
844
|
+
try:
|
|
845
|
+
parsed = _parse_json(data)
|
|
846
|
+
if not parsed:
|
|
847
|
+
log_debug(f"{cls.__name__}.from_dict: _parse_json returned None for data={_truncate_for_log(data)}")
|
|
848
|
+
return None
|
|
849
|
+
|
|
850
|
+
return cls(summary=_safe_get(parsed, "summary") or "")
|
|
851
|
+
except Exception as e:
|
|
852
|
+
log_debug(f"{cls.__name__}.from_dict failed: {e}, data={_truncate_for_log(data)}")
|
|
853
|
+
return None
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
@dataclass
|
|
857
|
+
class SessionPlanningExtractionResponse:
|
|
858
|
+
"""Response model for full planning extraction from LLM."""
|
|
859
|
+
|
|
860
|
+
summary: str = ""
|
|
861
|
+
goal: Optional[str] = None
|
|
862
|
+
plan: Optional[List[str]] = None
|
|
863
|
+
progress: Optional[List[str]] = None
|
|
864
|
+
|
|
865
|
+
@classmethod
|
|
866
|
+
def from_dict(cls, data: Any) -> Optional["SessionPlanningExtractionResponse"]:
|
|
867
|
+
"""Parse from dict/JSON, returning None on any failure."""
|
|
868
|
+
if data is None:
|
|
869
|
+
return None
|
|
870
|
+
if isinstance(data, cls):
|
|
871
|
+
return data
|
|
872
|
+
|
|
873
|
+
try:
|
|
874
|
+
parsed = _parse_json(data)
|
|
875
|
+
if not parsed:
|
|
876
|
+
log_debug(f"{cls.__name__}.from_dict: _parse_json returned None for data={_truncate_for_log(data)}")
|
|
877
|
+
return None
|
|
878
|
+
|
|
879
|
+
return cls(
|
|
880
|
+
summary=_safe_get(parsed, "summary") or "",
|
|
881
|
+
goal=_safe_get(parsed, "goal"),
|
|
882
|
+
plan=_safe_get(parsed, "plan"),
|
|
883
|
+
progress=_safe_get(parsed, "progress"),
|
|
884
|
+
)
|
|
885
|
+
except Exception as e:
|
|
886
|
+
log_debug(f"{cls.__name__}.from_dict failed: {e}, data={_truncate_for_log(data)}")
|
|
887
|
+
return None
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
# =============================================================================
|
|
891
|
+
# Phase 2 Schemas (Placeholders)
|
|
892
|
+
# =============================================================================
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
@dataclass
|
|
896
|
+
class DecisionLog:
|
|
897
|
+
"""Schema for Decision Logs.
|
|
898
|
+
|
|
899
|
+
Records decisions made by the agent with reasoning and context.
|
|
900
|
+
Useful for:
|
|
901
|
+
- Auditing agent behavior
|
|
902
|
+
- Learning from past decisions
|
|
903
|
+
- Debugging unexpected outcomes
|
|
904
|
+
- Building feedback loops
|
|
905
|
+
|
|
906
|
+
Example:
|
|
907
|
+
DecisionLog(
|
|
908
|
+
id="dec_abc123",
|
|
909
|
+
decision="Used web search instead of knowledge base",
|
|
910
|
+
reasoning="User asked about current events which require fresh data",
|
|
911
|
+
decision_type="tool_selection",
|
|
912
|
+
context="User query: 'What happened in the news today?'",
|
|
913
|
+
alternatives=["search_knowledge_base", "ask_for_clarification"],
|
|
914
|
+
confidence=0.85,
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
Attributes:
|
|
918
|
+
id: Unique identifier for this decision.
|
|
919
|
+
decision: What was decided (the choice made).
|
|
920
|
+
reasoning: Why this decision was made.
|
|
921
|
+
decision_type: Category of decision (tool_selection, response_style, etc).
|
|
922
|
+
context: The situation that required a decision.
|
|
923
|
+
alternatives: Other options that were considered.
|
|
924
|
+
confidence: How confident the agent was (0.0 to 1.0).
|
|
925
|
+
outcome: What happened as a result (can be updated later).
|
|
926
|
+
outcome_quality: Was the outcome good/bad/neutral.
|
|
927
|
+
tags: Categories for organization.
|
|
928
|
+
session_id: Which session this decision was made in.
|
|
929
|
+
user_id: Which user this decision was for.
|
|
930
|
+
agent_id: Which agent made this decision.
|
|
931
|
+
team_id: Which team context.
|
|
932
|
+
created_at: When the decision was made.
|
|
933
|
+
updated_at: When the outcome was recorded.
|
|
934
|
+
"""
|
|
935
|
+
|
|
936
|
+
id: str
|
|
937
|
+
decision: str
|
|
938
|
+
reasoning: Optional[str] = field(default=None, metadata={"description": "Why this decision was made"})
|
|
939
|
+
decision_type: Optional[str] = field(
|
|
940
|
+
default=None,
|
|
941
|
+
metadata={"description": "Category: tool_selection, response_style, clarification, escalation, etc"},
|
|
942
|
+
)
|
|
943
|
+
context: Optional[str] = field(default=None, metadata={"description": "The situation that required a decision"})
|
|
944
|
+
alternatives: Optional[List[str]] = field(
|
|
945
|
+
default=None, metadata={"description": "Other options that were considered"}
|
|
946
|
+
)
|
|
947
|
+
confidence: Optional[float] = field(default=None, metadata={"description": "Confidence level 0.0 to 1.0"})
|
|
948
|
+
outcome: Optional[str] = field(default=None, metadata={"description": "What happened as a result"})
|
|
949
|
+
outcome_quality: Optional[str] = field(default=None, metadata={"description": "Was outcome good/bad/neutral"})
|
|
950
|
+
tags: Optional[List[str]] = field(default=None, metadata={"description": "Categories for organization"})
|
|
951
|
+
|
|
952
|
+
# Scope
|
|
953
|
+
session_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
954
|
+
user_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
955
|
+
agent_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
956
|
+
team_id: Optional[str] = field(default=None, metadata={"internal": True})
|
|
957
|
+
created_at: Optional[str] = field(default=None, metadata={"internal": True})
|
|
958
|
+
updated_at: Optional[str] = field(default=None, metadata={"internal": True})
|
|
959
|
+
|
|
960
|
+
@classmethod
|
|
961
|
+
def from_dict(cls, data: Any) -> Optional["DecisionLog"]:
|
|
962
|
+
"""Parse from dict/JSON, returning None on any failure."""
|
|
963
|
+
if data is None:
|
|
964
|
+
return None
|
|
965
|
+
if isinstance(data, cls):
|
|
966
|
+
return data
|
|
967
|
+
|
|
968
|
+
try:
|
|
969
|
+
parsed = _parse_json(data)
|
|
970
|
+
if not parsed:
|
|
971
|
+
log_debug(f"{cls.__name__}.from_dict: _parse_json returned None for data={_truncate_for_log(data)}")
|
|
972
|
+
return None
|
|
973
|
+
|
|
974
|
+
# id and decision are required
|
|
975
|
+
if not parsed.get("id") or not parsed.get("decision"):
|
|
976
|
+
log_debug(f"{cls.__name__}.from_dict: missing required fields 'id' or 'decision'")
|
|
977
|
+
return None
|
|
978
|
+
|
|
979
|
+
field_names = {f.name for f in fields(cls)}
|
|
980
|
+
kwargs = {k: v for k, v in parsed.items() if k in field_names}
|
|
981
|
+
|
|
982
|
+
return cls(**kwargs)
|
|
983
|
+
except Exception as e:
|
|
984
|
+
log_debug(f"{cls.__name__}.from_dict failed: {e}, data={_truncate_for_log(data)}")
|
|
985
|
+
return None
|
|
986
|
+
|
|
987
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
988
|
+
"""Convert to dict."""
|
|
989
|
+
try:
|
|
990
|
+
return asdict(self)
|
|
991
|
+
except Exception as e:
|
|
992
|
+
log_debug(f"{self.__class__.__name__}.to_dict failed: {e}")
|
|
993
|
+
return {}
|
|
994
|
+
|
|
995
|
+
def to_text(self) -> str:
|
|
996
|
+
"""Convert to searchable text format."""
|
|
997
|
+
parts = [f"Decision: {self.decision}"]
|
|
998
|
+
if self.reasoning:
|
|
999
|
+
parts.append(f"Reasoning: {self.reasoning}")
|
|
1000
|
+
if self.context:
|
|
1001
|
+
parts.append(f"Context: {self.context}")
|
|
1002
|
+
if self.decision_type:
|
|
1003
|
+
parts.append(f"Type: {self.decision_type}")
|
|
1004
|
+
if self.outcome:
|
|
1005
|
+
parts.append(f"Outcome: {self.outcome}")
|
|
1006
|
+
return "\n".join(parts)
|
|
1007
|
+
|
|
1008
|
+
def __repr__(self) -> str:
|
|
1009
|
+
return f"DecisionLog(id={self.id}, decision={self.decision[:50]}...)"
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
# Backwards compatibility alias
|
|
1013
|
+
Decision = DecisionLog
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
@dataclass
|
|
1017
|
+
class Feedback:
|
|
1018
|
+
"""Schema for Behavioral Feedback. (Phase 2)
|
|
1019
|
+
|
|
1020
|
+
Captures signals about what worked and what didn't.
|
|
1021
|
+
"""
|
|
1022
|
+
|
|
1023
|
+
signal: str # thumbs_up, thumbs_down, correction, regeneration
|
|
1024
|
+
learning: Optional[str] = None
|
|
1025
|
+
context: Optional[str] = None
|
|
1026
|
+
agent_id: Optional[str] = None
|
|
1027
|
+
team_id: Optional[str] = None
|
|
1028
|
+
created_at: Optional[str] = None
|
|
1029
|
+
|
|
1030
|
+
@classmethod
|
|
1031
|
+
def from_dict(cls, data: Any) -> Optional["Feedback"]:
|
|
1032
|
+
"""Parse from dict/JSON, returning None on any failure."""
|
|
1033
|
+
if data is None:
|
|
1034
|
+
return None
|
|
1035
|
+
if isinstance(data, cls):
|
|
1036
|
+
return data
|
|
1037
|
+
|
|
1038
|
+
try:
|
|
1039
|
+
parsed = _parse_json(data)
|
|
1040
|
+
if not parsed:
|
|
1041
|
+
log_debug(f"{cls.__name__}.from_dict: _parse_json returned None for data={_truncate_for_log(data)}")
|
|
1042
|
+
return None
|
|
1043
|
+
|
|
1044
|
+
if not parsed.get("signal"):
|
|
1045
|
+
log_debug(f"{cls.__name__}.from_dict: missing required field 'signal'")
|
|
1046
|
+
return None
|
|
1047
|
+
|
|
1048
|
+
field_names = {f.name for f in fields(cls)}
|
|
1049
|
+
kwargs = {k: v for k, v in parsed.items() if k in field_names}
|
|
1050
|
+
|
|
1051
|
+
return cls(**kwargs)
|
|
1052
|
+
except Exception as e:
|
|
1053
|
+
log_debug(f"{cls.__name__}.from_dict failed: {e}, data={_truncate_for_log(data)}")
|
|
1054
|
+
return None
|
|
1055
|
+
|
|
1056
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1057
|
+
"""Convert to dict."""
|
|
1058
|
+
try:
|
|
1059
|
+
return asdict(self)
|
|
1060
|
+
except Exception as e:
|
|
1061
|
+
log_debug(f"{self.__class__.__name__}.to_dict failed: {e}")
|
|
1062
|
+
return {}
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
@dataclass
|
|
1066
|
+
class InstructionUpdate:
|
|
1067
|
+
"""Schema for Self-Improvement. (Phase 3)
|
|
1068
|
+
|
|
1069
|
+
Proposes updates to agent instructions based on feedback patterns.
|
|
1070
|
+
"""
|
|
1071
|
+
|
|
1072
|
+
current_instruction: str
|
|
1073
|
+
proposed_instruction: str
|
|
1074
|
+
reasoning: str
|
|
1075
|
+
evidence: Optional[List[str]] = None
|
|
1076
|
+
agent_id: Optional[str] = None
|
|
1077
|
+
team_id: Optional[str] = None
|
|
1078
|
+
created_at: Optional[str] = None
|
|
1079
|
+
|
|
1080
|
+
@classmethod
|
|
1081
|
+
def from_dict(cls, data: Any) -> Optional["InstructionUpdate"]:
|
|
1082
|
+
"""Parse from dict/JSON, returning None on any failure."""
|
|
1083
|
+
if data is None:
|
|
1084
|
+
return None
|
|
1085
|
+
if isinstance(data, cls):
|
|
1086
|
+
return data
|
|
1087
|
+
|
|
1088
|
+
try:
|
|
1089
|
+
parsed = _parse_json(data)
|
|
1090
|
+
if not parsed:
|
|
1091
|
+
log_debug(f"{cls.__name__}.from_dict: _parse_json returned None for data={_truncate_for_log(data)}")
|
|
1092
|
+
return None
|
|
1093
|
+
|
|
1094
|
+
required = ["current_instruction", "proposed_instruction", "reasoning"]
|
|
1095
|
+
missing = [k for k in required if not parsed.get(k)]
|
|
1096
|
+
if missing:
|
|
1097
|
+
log_debug(f"{cls.__name__}.from_dict: missing required fields {missing}")
|
|
1098
|
+
return None
|
|
1099
|
+
|
|
1100
|
+
field_names = {f.name for f in fields(cls)}
|
|
1101
|
+
kwargs = {k: v for k, v in parsed.items() if k in field_names}
|
|
1102
|
+
|
|
1103
|
+
return cls(**kwargs)
|
|
1104
|
+
except Exception as e:
|
|
1105
|
+
log_debug(f"{cls.__name__}.from_dict failed: {e}, data={_truncate_for_log(data)}")
|
|
1106
|
+
return None
|
|
1107
|
+
|
|
1108
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1109
|
+
"""Convert to dict."""
|
|
1110
|
+
try:
|
|
1111
|
+
return asdict(self)
|
|
1112
|
+
except Exception as e:
|
|
1113
|
+
log_debug(f"{self.__class__.__name__}.to_dict failed: {e}")
|
|
1114
|
+
return {}
|