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.
Files changed (383) hide show
  1. agno/agent/__init__.py +6 -0
  2. agno/agent/agent.py +5252 -3145
  3. agno/agent/remote.py +525 -0
  4. agno/api/api.py +2 -0
  5. agno/client/__init__.py +3 -0
  6. agno/client/a2a/__init__.py +10 -0
  7. agno/client/a2a/client.py +554 -0
  8. agno/client/a2a/schemas.py +112 -0
  9. agno/client/a2a/utils.py +369 -0
  10. agno/client/os.py +2669 -0
  11. agno/compression/__init__.py +3 -0
  12. agno/compression/manager.py +247 -0
  13. agno/culture/manager.py +2 -2
  14. agno/db/base.py +927 -6
  15. agno/db/dynamo/dynamo.py +788 -2
  16. agno/db/dynamo/schemas.py +128 -0
  17. agno/db/dynamo/utils.py +26 -3
  18. agno/db/firestore/firestore.py +674 -50
  19. agno/db/firestore/schemas.py +41 -0
  20. agno/db/firestore/utils.py +25 -10
  21. agno/db/gcs_json/gcs_json_db.py +506 -3
  22. agno/db/gcs_json/utils.py +14 -2
  23. agno/db/in_memory/in_memory_db.py +203 -4
  24. agno/db/in_memory/utils.py +14 -2
  25. agno/db/json/json_db.py +498 -2
  26. agno/db/json/utils.py +14 -2
  27. agno/db/migrations/manager.py +199 -0
  28. agno/db/migrations/utils.py +19 -0
  29. agno/db/migrations/v1_to_v2.py +54 -16
  30. agno/db/migrations/versions/__init__.py +0 -0
  31. agno/db/migrations/versions/v2_3_0.py +977 -0
  32. agno/db/mongo/async_mongo.py +1013 -39
  33. agno/db/mongo/mongo.py +684 -4
  34. agno/db/mongo/schemas.py +48 -0
  35. agno/db/mongo/utils.py +17 -0
  36. agno/db/mysql/__init__.py +2 -1
  37. agno/db/mysql/async_mysql.py +2958 -0
  38. agno/db/mysql/mysql.py +722 -53
  39. agno/db/mysql/schemas.py +77 -11
  40. agno/db/mysql/utils.py +151 -8
  41. agno/db/postgres/async_postgres.py +1254 -137
  42. agno/db/postgres/postgres.py +2316 -93
  43. agno/db/postgres/schemas.py +153 -21
  44. agno/db/postgres/utils.py +22 -7
  45. agno/db/redis/redis.py +531 -3
  46. agno/db/redis/schemas.py +36 -0
  47. agno/db/redis/utils.py +31 -15
  48. agno/db/schemas/evals.py +1 -0
  49. agno/db/schemas/memory.py +20 -9
  50. agno/db/singlestore/schemas.py +70 -1
  51. agno/db/singlestore/singlestore.py +737 -74
  52. agno/db/singlestore/utils.py +13 -3
  53. agno/db/sqlite/async_sqlite.py +1069 -89
  54. agno/db/sqlite/schemas.py +133 -1
  55. agno/db/sqlite/sqlite.py +2203 -165
  56. agno/db/sqlite/utils.py +21 -11
  57. agno/db/surrealdb/models.py +25 -0
  58. agno/db/surrealdb/surrealdb.py +603 -1
  59. agno/db/utils.py +60 -0
  60. agno/eval/__init__.py +26 -3
  61. agno/eval/accuracy.py +25 -12
  62. agno/eval/agent_as_judge.py +871 -0
  63. agno/eval/base.py +29 -0
  64. agno/eval/performance.py +10 -4
  65. agno/eval/reliability.py +22 -13
  66. agno/eval/utils.py +2 -1
  67. agno/exceptions.py +42 -0
  68. agno/hooks/__init__.py +3 -0
  69. agno/hooks/decorator.py +164 -0
  70. agno/integrations/discord/client.py +13 -2
  71. agno/knowledge/__init__.py +4 -0
  72. agno/knowledge/chunking/code.py +90 -0
  73. agno/knowledge/chunking/document.py +65 -4
  74. agno/knowledge/chunking/fixed.py +4 -1
  75. agno/knowledge/chunking/markdown.py +102 -11
  76. agno/knowledge/chunking/recursive.py +2 -2
  77. agno/knowledge/chunking/semantic.py +130 -48
  78. agno/knowledge/chunking/strategy.py +18 -0
  79. agno/knowledge/embedder/azure_openai.py +0 -1
  80. agno/knowledge/embedder/google.py +1 -1
  81. agno/knowledge/embedder/mistral.py +1 -1
  82. agno/knowledge/embedder/nebius.py +1 -1
  83. agno/knowledge/embedder/openai.py +16 -12
  84. agno/knowledge/filesystem.py +412 -0
  85. agno/knowledge/knowledge.py +4261 -1199
  86. agno/knowledge/protocol.py +134 -0
  87. agno/knowledge/reader/arxiv_reader.py +3 -2
  88. agno/knowledge/reader/base.py +9 -7
  89. agno/knowledge/reader/csv_reader.py +91 -42
  90. agno/knowledge/reader/docx_reader.py +9 -10
  91. agno/knowledge/reader/excel_reader.py +225 -0
  92. agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
  93. agno/knowledge/reader/firecrawl_reader.py +3 -2
  94. agno/knowledge/reader/json_reader.py +16 -22
  95. agno/knowledge/reader/markdown_reader.py +15 -14
  96. agno/knowledge/reader/pdf_reader.py +33 -28
  97. agno/knowledge/reader/pptx_reader.py +9 -10
  98. agno/knowledge/reader/reader_factory.py +135 -1
  99. agno/knowledge/reader/s3_reader.py +8 -16
  100. agno/knowledge/reader/tavily_reader.py +3 -3
  101. agno/knowledge/reader/text_reader.py +15 -14
  102. agno/knowledge/reader/utils/__init__.py +17 -0
  103. agno/knowledge/reader/utils/spreadsheet.py +114 -0
  104. agno/knowledge/reader/web_search_reader.py +8 -65
  105. agno/knowledge/reader/website_reader.py +16 -13
  106. agno/knowledge/reader/wikipedia_reader.py +36 -3
  107. agno/knowledge/reader/youtube_reader.py +3 -2
  108. agno/knowledge/remote_content/__init__.py +33 -0
  109. agno/knowledge/remote_content/config.py +266 -0
  110. agno/knowledge/remote_content/remote_content.py +105 -17
  111. agno/knowledge/utils.py +76 -22
  112. agno/learn/__init__.py +71 -0
  113. agno/learn/config.py +463 -0
  114. agno/learn/curate.py +185 -0
  115. agno/learn/machine.py +725 -0
  116. agno/learn/schemas.py +1114 -0
  117. agno/learn/stores/__init__.py +38 -0
  118. agno/learn/stores/decision_log.py +1156 -0
  119. agno/learn/stores/entity_memory.py +3275 -0
  120. agno/learn/stores/learned_knowledge.py +1583 -0
  121. agno/learn/stores/protocol.py +117 -0
  122. agno/learn/stores/session_context.py +1217 -0
  123. agno/learn/stores/user_memory.py +1495 -0
  124. agno/learn/stores/user_profile.py +1220 -0
  125. agno/learn/utils.py +209 -0
  126. agno/media.py +22 -6
  127. agno/memory/__init__.py +14 -1
  128. agno/memory/manager.py +223 -8
  129. agno/memory/strategies/__init__.py +15 -0
  130. agno/memory/strategies/base.py +66 -0
  131. agno/memory/strategies/summarize.py +196 -0
  132. agno/memory/strategies/types.py +37 -0
  133. agno/models/aimlapi/aimlapi.py +17 -0
  134. agno/models/anthropic/claude.py +434 -59
  135. agno/models/aws/bedrock.py +121 -20
  136. agno/models/aws/claude.py +131 -274
  137. agno/models/azure/ai_foundry.py +10 -6
  138. agno/models/azure/openai_chat.py +33 -10
  139. agno/models/base.py +1162 -561
  140. agno/models/cerebras/cerebras.py +120 -24
  141. agno/models/cerebras/cerebras_openai.py +21 -2
  142. agno/models/cohere/chat.py +65 -6
  143. agno/models/cometapi/cometapi.py +18 -1
  144. agno/models/dashscope/dashscope.py +2 -3
  145. agno/models/deepinfra/deepinfra.py +18 -1
  146. agno/models/deepseek/deepseek.py +69 -3
  147. agno/models/fireworks/fireworks.py +18 -1
  148. agno/models/google/gemini.py +959 -89
  149. agno/models/google/utils.py +22 -0
  150. agno/models/groq/groq.py +48 -18
  151. agno/models/huggingface/huggingface.py +17 -6
  152. agno/models/ibm/watsonx.py +16 -6
  153. agno/models/internlm/internlm.py +18 -1
  154. agno/models/langdb/langdb.py +13 -1
  155. agno/models/litellm/chat.py +88 -9
  156. agno/models/litellm/litellm_openai.py +18 -1
  157. agno/models/message.py +24 -5
  158. agno/models/meta/llama.py +40 -13
  159. agno/models/meta/llama_openai.py +22 -21
  160. agno/models/metrics.py +12 -0
  161. agno/models/mistral/mistral.py +8 -4
  162. agno/models/n1n/__init__.py +3 -0
  163. agno/models/n1n/n1n.py +57 -0
  164. agno/models/nebius/nebius.py +6 -7
  165. agno/models/nvidia/nvidia.py +20 -3
  166. agno/models/ollama/__init__.py +2 -0
  167. agno/models/ollama/chat.py +17 -6
  168. agno/models/ollama/responses.py +100 -0
  169. agno/models/openai/__init__.py +2 -0
  170. agno/models/openai/chat.py +117 -26
  171. agno/models/openai/open_responses.py +46 -0
  172. agno/models/openai/responses.py +110 -32
  173. agno/models/openrouter/__init__.py +2 -0
  174. agno/models/openrouter/openrouter.py +67 -2
  175. agno/models/openrouter/responses.py +146 -0
  176. agno/models/perplexity/perplexity.py +19 -1
  177. agno/models/portkey/portkey.py +7 -6
  178. agno/models/requesty/requesty.py +19 -2
  179. agno/models/response.py +20 -2
  180. agno/models/sambanova/sambanova.py +20 -3
  181. agno/models/siliconflow/siliconflow.py +19 -2
  182. agno/models/together/together.py +20 -3
  183. agno/models/vercel/v0.py +20 -3
  184. agno/models/vertexai/claude.py +124 -4
  185. agno/models/vllm/vllm.py +19 -14
  186. agno/models/xai/xai.py +19 -2
  187. agno/os/app.py +467 -137
  188. agno/os/auth.py +253 -5
  189. agno/os/config.py +22 -0
  190. agno/os/interfaces/a2a/a2a.py +7 -6
  191. agno/os/interfaces/a2a/router.py +635 -26
  192. agno/os/interfaces/a2a/utils.py +32 -33
  193. agno/os/interfaces/agui/agui.py +5 -3
  194. agno/os/interfaces/agui/router.py +26 -16
  195. agno/os/interfaces/agui/utils.py +97 -57
  196. agno/os/interfaces/base.py +7 -7
  197. agno/os/interfaces/slack/router.py +16 -7
  198. agno/os/interfaces/slack/slack.py +7 -7
  199. agno/os/interfaces/whatsapp/router.py +35 -7
  200. agno/os/interfaces/whatsapp/security.py +3 -1
  201. agno/os/interfaces/whatsapp/whatsapp.py +11 -8
  202. agno/os/managers.py +326 -0
  203. agno/os/mcp.py +652 -79
  204. agno/os/middleware/__init__.py +4 -0
  205. agno/os/middleware/jwt.py +718 -115
  206. agno/os/middleware/trailing_slash.py +27 -0
  207. agno/os/router.py +105 -1558
  208. agno/os/routers/agents/__init__.py +3 -0
  209. agno/os/routers/agents/router.py +655 -0
  210. agno/os/routers/agents/schema.py +288 -0
  211. agno/os/routers/components/__init__.py +3 -0
  212. agno/os/routers/components/components.py +475 -0
  213. agno/os/routers/database.py +155 -0
  214. agno/os/routers/evals/evals.py +111 -18
  215. agno/os/routers/evals/schemas.py +38 -5
  216. agno/os/routers/evals/utils.py +80 -11
  217. agno/os/routers/health.py +3 -3
  218. agno/os/routers/knowledge/knowledge.py +284 -35
  219. agno/os/routers/knowledge/schemas.py +14 -2
  220. agno/os/routers/memory/memory.py +274 -11
  221. agno/os/routers/memory/schemas.py +44 -3
  222. agno/os/routers/metrics/metrics.py +30 -15
  223. agno/os/routers/metrics/schemas.py +10 -6
  224. agno/os/routers/registry/__init__.py +3 -0
  225. agno/os/routers/registry/registry.py +337 -0
  226. agno/os/routers/session/session.py +143 -14
  227. agno/os/routers/teams/__init__.py +3 -0
  228. agno/os/routers/teams/router.py +550 -0
  229. agno/os/routers/teams/schema.py +280 -0
  230. agno/os/routers/traces/__init__.py +3 -0
  231. agno/os/routers/traces/schemas.py +414 -0
  232. agno/os/routers/traces/traces.py +549 -0
  233. agno/os/routers/workflows/__init__.py +3 -0
  234. agno/os/routers/workflows/router.py +757 -0
  235. agno/os/routers/workflows/schema.py +139 -0
  236. agno/os/schema.py +157 -584
  237. agno/os/scopes.py +469 -0
  238. agno/os/settings.py +3 -0
  239. agno/os/utils.py +574 -185
  240. agno/reasoning/anthropic.py +85 -1
  241. agno/reasoning/azure_ai_foundry.py +93 -1
  242. agno/reasoning/deepseek.py +102 -2
  243. agno/reasoning/default.py +6 -7
  244. agno/reasoning/gemini.py +87 -3
  245. agno/reasoning/groq.py +109 -2
  246. agno/reasoning/helpers.py +6 -7
  247. agno/reasoning/manager.py +1238 -0
  248. agno/reasoning/ollama.py +93 -1
  249. agno/reasoning/openai.py +115 -1
  250. agno/reasoning/vertexai.py +85 -1
  251. agno/registry/__init__.py +3 -0
  252. agno/registry/registry.py +68 -0
  253. agno/remote/__init__.py +3 -0
  254. agno/remote/base.py +581 -0
  255. agno/run/__init__.py +2 -4
  256. agno/run/agent.py +134 -19
  257. agno/run/base.py +49 -1
  258. agno/run/cancel.py +65 -52
  259. agno/run/cancellation_management/__init__.py +9 -0
  260. agno/run/cancellation_management/base.py +78 -0
  261. agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
  262. agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
  263. agno/run/requirement.py +181 -0
  264. agno/run/team.py +111 -19
  265. agno/run/workflow.py +2 -1
  266. agno/session/agent.py +57 -92
  267. agno/session/summary.py +1 -1
  268. agno/session/team.py +62 -115
  269. agno/session/workflow.py +353 -57
  270. agno/skills/__init__.py +17 -0
  271. agno/skills/agent_skills.py +377 -0
  272. agno/skills/errors.py +32 -0
  273. agno/skills/loaders/__init__.py +4 -0
  274. agno/skills/loaders/base.py +27 -0
  275. agno/skills/loaders/local.py +216 -0
  276. agno/skills/skill.py +65 -0
  277. agno/skills/utils.py +107 -0
  278. agno/skills/validator.py +277 -0
  279. agno/table.py +10 -0
  280. agno/team/__init__.py +5 -1
  281. agno/team/remote.py +447 -0
  282. agno/team/team.py +3769 -2202
  283. agno/tools/brandfetch.py +27 -18
  284. agno/tools/browserbase.py +225 -16
  285. agno/tools/crawl4ai.py +3 -0
  286. agno/tools/duckduckgo.py +25 -71
  287. agno/tools/exa.py +0 -21
  288. agno/tools/file.py +14 -13
  289. agno/tools/file_generation.py +12 -6
  290. agno/tools/firecrawl.py +15 -7
  291. agno/tools/function.py +94 -113
  292. agno/tools/google_bigquery.py +11 -2
  293. agno/tools/google_drive.py +4 -3
  294. agno/tools/knowledge.py +9 -4
  295. agno/tools/mcp/mcp.py +301 -18
  296. agno/tools/mcp/multi_mcp.py +269 -14
  297. agno/tools/mem0.py +11 -10
  298. agno/tools/memory.py +47 -46
  299. agno/tools/mlx_transcribe.py +10 -7
  300. agno/tools/models/nebius.py +5 -5
  301. agno/tools/models_labs.py +20 -10
  302. agno/tools/nano_banana.py +151 -0
  303. agno/tools/parallel.py +0 -7
  304. agno/tools/postgres.py +76 -36
  305. agno/tools/python.py +14 -6
  306. agno/tools/reasoning.py +30 -23
  307. agno/tools/redshift.py +406 -0
  308. agno/tools/shopify.py +1519 -0
  309. agno/tools/spotify.py +919 -0
  310. agno/tools/tavily.py +4 -1
  311. agno/tools/toolkit.py +253 -18
  312. agno/tools/websearch.py +93 -0
  313. agno/tools/website.py +1 -1
  314. agno/tools/wikipedia.py +1 -1
  315. agno/tools/workflow.py +56 -48
  316. agno/tools/yfinance.py +12 -11
  317. agno/tracing/__init__.py +12 -0
  318. agno/tracing/exporter.py +161 -0
  319. agno/tracing/schemas.py +276 -0
  320. agno/tracing/setup.py +112 -0
  321. agno/utils/agent.py +251 -10
  322. agno/utils/cryptography.py +22 -0
  323. agno/utils/dttm.py +33 -0
  324. agno/utils/events.py +264 -7
  325. agno/utils/hooks.py +111 -3
  326. agno/utils/http.py +161 -2
  327. agno/utils/mcp.py +49 -8
  328. agno/utils/media.py +22 -1
  329. agno/utils/models/ai_foundry.py +9 -2
  330. agno/utils/models/claude.py +20 -5
  331. agno/utils/models/cohere.py +9 -2
  332. agno/utils/models/llama.py +9 -2
  333. agno/utils/models/mistral.py +4 -2
  334. agno/utils/os.py +0 -0
  335. agno/utils/print_response/agent.py +99 -16
  336. agno/utils/print_response/team.py +223 -24
  337. agno/utils/print_response/workflow.py +0 -2
  338. agno/utils/prompts.py +8 -6
  339. agno/utils/remote.py +23 -0
  340. agno/utils/response.py +1 -13
  341. agno/utils/string.py +91 -2
  342. agno/utils/team.py +62 -12
  343. agno/utils/tokens.py +657 -0
  344. agno/vectordb/base.py +15 -2
  345. agno/vectordb/cassandra/cassandra.py +1 -1
  346. agno/vectordb/chroma/__init__.py +2 -1
  347. agno/vectordb/chroma/chromadb.py +468 -23
  348. agno/vectordb/clickhouse/clickhousedb.py +1 -1
  349. agno/vectordb/couchbase/couchbase.py +6 -2
  350. agno/vectordb/lancedb/lance_db.py +7 -38
  351. agno/vectordb/lightrag/lightrag.py +7 -6
  352. agno/vectordb/milvus/milvus.py +118 -84
  353. agno/vectordb/mongodb/__init__.py +2 -1
  354. agno/vectordb/mongodb/mongodb.py +14 -31
  355. agno/vectordb/pgvector/pgvector.py +120 -66
  356. agno/vectordb/pineconedb/pineconedb.py +2 -19
  357. agno/vectordb/qdrant/__init__.py +2 -1
  358. agno/vectordb/qdrant/qdrant.py +33 -56
  359. agno/vectordb/redis/__init__.py +2 -1
  360. agno/vectordb/redis/redisdb.py +19 -31
  361. agno/vectordb/singlestore/singlestore.py +17 -9
  362. agno/vectordb/surrealdb/surrealdb.py +2 -38
  363. agno/vectordb/weaviate/__init__.py +2 -1
  364. agno/vectordb/weaviate/weaviate.py +7 -3
  365. agno/workflow/__init__.py +5 -1
  366. agno/workflow/agent.py +2 -2
  367. agno/workflow/condition.py +12 -10
  368. agno/workflow/loop.py +28 -9
  369. agno/workflow/parallel.py +21 -13
  370. agno/workflow/remote.py +362 -0
  371. agno/workflow/router.py +12 -9
  372. agno/workflow/step.py +261 -36
  373. agno/workflow/steps.py +12 -8
  374. agno/workflow/types.py +40 -77
  375. agno/workflow/workflow.py +939 -213
  376. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
  377. agno-2.4.3.dist-info/RECORD +677 -0
  378. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
  379. agno/tools/googlesearch.py +0 -98
  380. agno/tools/memori.py +0 -339
  381. agno-2.2.13.dist-info/RECORD +0 -575
  382. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
  383. {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 {}