vanna 0.7.9__py3-none-any.whl → 2.0.0__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 (302) hide show
  1. vanna/__init__.py +167 -395
  2. vanna/agents/__init__.py +7 -0
  3. vanna/capabilities/__init__.py +17 -0
  4. vanna/capabilities/agent_memory/__init__.py +21 -0
  5. vanna/capabilities/agent_memory/base.py +103 -0
  6. vanna/capabilities/agent_memory/models.py +53 -0
  7. vanna/capabilities/file_system/__init__.py +14 -0
  8. vanna/capabilities/file_system/base.py +71 -0
  9. vanna/capabilities/file_system/models.py +25 -0
  10. vanna/capabilities/sql_runner/__init__.py +13 -0
  11. vanna/capabilities/sql_runner/base.py +37 -0
  12. vanna/capabilities/sql_runner/models.py +13 -0
  13. vanna/components/__init__.py +92 -0
  14. vanna/components/base.py +11 -0
  15. vanna/components/rich/__init__.py +83 -0
  16. vanna/components/rich/containers/__init__.py +7 -0
  17. vanna/components/rich/containers/card.py +20 -0
  18. vanna/components/rich/data/__init__.py +9 -0
  19. vanna/components/rich/data/chart.py +17 -0
  20. vanna/components/rich/data/dataframe.py +93 -0
  21. vanna/components/rich/feedback/__init__.py +21 -0
  22. vanna/components/rich/feedback/badge.py +16 -0
  23. vanna/components/rich/feedback/icon_text.py +14 -0
  24. vanna/components/rich/feedback/log_viewer.py +41 -0
  25. vanna/components/rich/feedback/notification.py +19 -0
  26. vanna/components/rich/feedback/progress.py +37 -0
  27. vanna/components/rich/feedback/status_card.py +28 -0
  28. vanna/components/rich/feedback/status_indicator.py +14 -0
  29. vanna/components/rich/interactive/__init__.py +21 -0
  30. vanna/components/rich/interactive/button.py +95 -0
  31. vanna/components/rich/interactive/task_list.py +58 -0
  32. vanna/components/rich/interactive/ui_state.py +93 -0
  33. vanna/components/rich/specialized/__init__.py +7 -0
  34. vanna/components/rich/specialized/artifact.py +20 -0
  35. vanna/components/rich/text.py +16 -0
  36. vanna/components/simple/__init__.py +15 -0
  37. vanna/components/simple/image.py +15 -0
  38. vanna/components/simple/link.py +15 -0
  39. vanna/components/simple/text.py +11 -0
  40. vanna/core/__init__.py +193 -0
  41. vanna/core/_compat.py +19 -0
  42. vanna/core/agent/__init__.py +10 -0
  43. vanna/core/agent/agent.py +1407 -0
  44. vanna/core/agent/config.py +123 -0
  45. vanna/core/audit/__init__.py +28 -0
  46. vanna/core/audit/base.py +299 -0
  47. vanna/core/audit/models.py +131 -0
  48. vanna/core/component_manager.py +329 -0
  49. vanna/core/components.py +53 -0
  50. vanna/core/enhancer/__init__.py +11 -0
  51. vanna/core/enhancer/base.py +94 -0
  52. vanna/core/enhancer/default.py +118 -0
  53. vanna/core/enricher/__init__.py +10 -0
  54. vanna/core/enricher/base.py +59 -0
  55. vanna/core/errors.py +47 -0
  56. vanna/core/evaluation/__init__.py +81 -0
  57. vanna/core/evaluation/base.py +186 -0
  58. vanna/core/evaluation/dataset.py +254 -0
  59. vanna/core/evaluation/evaluators.py +376 -0
  60. vanna/core/evaluation/report.py +289 -0
  61. vanna/core/evaluation/runner.py +313 -0
  62. vanna/core/filter/__init__.py +10 -0
  63. vanna/core/filter/base.py +67 -0
  64. vanna/core/lifecycle/__init__.py +10 -0
  65. vanna/core/lifecycle/base.py +83 -0
  66. vanna/core/llm/__init__.py +16 -0
  67. vanna/core/llm/base.py +40 -0
  68. vanna/core/llm/models.py +61 -0
  69. vanna/core/middleware/__init__.py +10 -0
  70. vanna/core/middleware/base.py +69 -0
  71. vanna/core/observability/__init__.py +11 -0
  72. vanna/core/observability/base.py +88 -0
  73. vanna/core/observability/models.py +47 -0
  74. vanna/core/recovery/__init__.py +11 -0
  75. vanna/core/recovery/base.py +84 -0
  76. vanna/core/recovery/models.py +32 -0
  77. vanna/core/registry.py +278 -0
  78. vanna/core/rich_component.py +156 -0
  79. vanna/core/simple_component.py +27 -0
  80. vanna/core/storage/__init__.py +14 -0
  81. vanna/core/storage/base.py +46 -0
  82. vanna/core/storage/models.py +46 -0
  83. vanna/core/system_prompt/__init__.py +13 -0
  84. vanna/core/system_prompt/base.py +36 -0
  85. vanna/core/system_prompt/default.py +157 -0
  86. vanna/core/tool/__init__.py +18 -0
  87. vanna/core/tool/base.py +70 -0
  88. vanna/core/tool/models.py +84 -0
  89. vanna/core/user/__init__.py +17 -0
  90. vanna/core/user/base.py +29 -0
  91. vanna/core/user/models.py +25 -0
  92. vanna/core/user/request_context.py +70 -0
  93. vanna/core/user/resolver.py +42 -0
  94. vanna/core/validation.py +164 -0
  95. vanna/core/workflow/__init__.py +12 -0
  96. vanna/core/workflow/base.py +254 -0
  97. vanna/core/workflow/default.py +789 -0
  98. vanna/examples/__init__.py +1 -0
  99. vanna/examples/__main__.py +44 -0
  100. vanna/examples/anthropic_quickstart.py +80 -0
  101. vanna/examples/artifact_example.py +293 -0
  102. vanna/examples/claude_sqlite_example.py +236 -0
  103. vanna/examples/coding_agent_example.py +300 -0
  104. vanna/examples/custom_system_prompt_example.py +174 -0
  105. vanna/examples/default_workflow_handler_example.py +208 -0
  106. vanna/examples/email_auth_example.py +340 -0
  107. vanna/examples/evaluation_example.py +269 -0
  108. vanna/examples/extensibility_example.py +262 -0
  109. vanna/examples/minimal_example.py +67 -0
  110. vanna/examples/mock_auth_example.py +227 -0
  111. vanna/examples/mock_custom_tool.py +311 -0
  112. vanna/examples/mock_quickstart.py +79 -0
  113. vanna/examples/mock_quota_example.py +145 -0
  114. vanna/examples/mock_rich_components_demo.py +396 -0
  115. vanna/examples/mock_sqlite_example.py +223 -0
  116. vanna/examples/openai_quickstart.py +83 -0
  117. vanna/examples/primitive_components_demo.py +305 -0
  118. vanna/examples/quota_lifecycle_example.py +139 -0
  119. vanna/examples/visualization_example.py +251 -0
  120. vanna/integrations/__init__.py +17 -0
  121. vanna/integrations/anthropic/__init__.py +9 -0
  122. vanna/integrations/anthropic/llm.py +270 -0
  123. vanna/integrations/azureopenai/__init__.py +9 -0
  124. vanna/integrations/azureopenai/llm.py +329 -0
  125. vanna/integrations/azuresearch/__init__.py +7 -0
  126. vanna/integrations/azuresearch/agent_memory.py +413 -0
  127. vanna/integrations/bigquery/__init__.py +5 -0
  128. vanna/integrations/bigquery/sql_runner.py +81 -0
  129. vanna/integrations/chromadb/__init__.py +104 -0
  130. vanna/integrations/chromadb/agent_memory.py +416 -0
  131. vanna/integrations/clickhouse/__init__.py +5 -0
  132. vanna/integrations/clickhouse/sql_runner.py +82 -0
  133. vanna/integrations/duckdb/__init__.py +5 -0
  134. vanna/integrations/duckdb/sql_runner.py +65 -0
  135. vanna/integrations/faiss/__init__.py +7 -0
  136. vanna/integrations/faiss/agent_memory.py +431 -0
  137. vanna/integrations/google/__init__.py +9 -0
  138. vanna/integrations/google/gemini.py +370 -0
  139. vanna/integrations/hive/__init__.py +5 -0
  140. vanna/integrations/hive/sql_runner.py +87 -0
  141. vanna/integrations/local/__init__.py +17 -0
  142. vanna/integrations/local/agent_memory/__init__.py +7 -0
  143. vanna/integrations/local/agent_memory/in_memory.py +285 -0
  144. vanna/integrations/local/audit.py +59 -0
  145. vanna/integrations/local/file_system.py +242 -0
  146. vanna/integrations/local/file_system_conversation_store.py +255 -0
  147. vanna/integrations/local/storage.py +62 -0
  148. vanna/integrations/marqo/__init__.py +7 -0
  149. vanna/integrations/marqo/agent_memory.py +354 -0
  150. vanna/integrations/milvus/__init__.py +7 -0
  151. vanna/integrations/milvus/agent_memory.py +458 -0
  152. vanna/integrations/mock/__init__.py +9 -0
  153. vanna/integrations/mock/llm.py +65 -0
  154. vanna/integrations/mssql/__init__.py +5 -0
  155. vanna/integrations/mssql/sql_runner.py +66 -0
  156. vanna/integrations/mysql/__init__.py +5 -0
  157. vanna/integrations/mysql/sql_runner.py +92 -0
  158. vanna/integrations/ollama/__init__.py +7 -0
  159. vanna/integrations/ollama/llm.py +252 -0
  160. vanna/integrations/openai/__init__.py +10 -0
  161. vanna/integrations/openai/llm.py +267 -0
  162. vanna/integrations/openai/responses.py +163 -0
  163. vanna/integrations/opensearch/__init__.py +7 -0
  164. vanna/integrations/opensearch/agent_memory.py +411 -0
  165. vanna/integrations/oracle/__init__.py +5 -0
  166. vanna/integrations/oracle/sql_runner.py +75 -0
  167. vanna/integrations/pinecone/__init__.py +7 -0
  168. vanna/integrations/pinecone/agent_memory.py +329 -0
  169. vanna/integrations/plotly/__init__.py +5 -0
  170. vanna/integrations/plotly/chart_generator.py +313 -0
  171. vanna/integrations/postgres/__init__.py +9 -0
  172. vanna/integrations/postgres/sql_runner.py +112 -0
  173. vanna/integrations/premium/agent_memory/__init__.py +7 -0
  174. vanna/integrations/premium/agent_memory/premium.py +186 -0
  175. vanna/integrations/presto/__init__.py +5 -0
  176. vanna/integrations/presto/sql_runner.py +107 -0
  177. vanna/integrations/qdrant/__init__.py +7 -0
  178. vanna/integrations/qdrant/agent_memory.py +461 -0
  179. vanna/integrations/snowflake/__init__.py +5 -0
  180. vanna/integrations/snowflake/sql_runner.py +147 -0
  181. vanna/integrations/sqlite/__init__.py +9 -0
  182. vanna/integrations/sqlite/sql_runner.py +65 -0
  183. vanna/integrations/weaviate/__init__.py +7 -0
  184. vanna/integrations/weaviate/agent_memory.py +428 -0
  185. vanna/{ZhipuAI → legacy/ZhipuAI}/ZhipuAI_embeddings.py +11 -11
  186. vanna/legacy/__init__.py +403 -0
  187. vanna/legacy/adapter.py +463 -0
  188. vanna/{advanced → legacy/advanced}/__init__.py +3 -1
  189. vanna/{anthropic → legacy/anthropic}/anthropic_chat.py +9 -7
  190. vanna/{azuresearch → legacy/azuresearch}/azuresearch_vector.py +79 -41
  191. vanna/{base → legacy/base}/base.py +224 -217
  192. vanna/legacy/bedrock/__init__.py +1 -0
  193. vanna/{bedrock → legacy/bedrock}/bedrock_converse.py +13 -12
  194. vanna/{chromadb → legacy/chromadb}/chromadb_vector.py +3 -1
  195. vanna/legacy/cohere/__init__.py +2 -0
  196. vanna/{cohere → legacy/cohere}/cohere_chat.py +19 -14
  197. vanna/{cohere → legacy/cohere}/cohere_embeddings.py +25 -19
  198. vanna/{deepseek → legacy/deepseek}/deepseek_chat.py +5 -6
  199. vanna/legacy/faiss/__init__.py +1 -0
  200. vanna/{faiss → legacy/faiss}/faiss.py +113 -59
  201. vanna/{flask → legacy/flask}/__init__.py +84 -43
  202. vanna/{flask → legacy/flask}/assets.py +5 -5
  203. vanna/{flask → legacy/flask}/auth.py +5 -4
  204. vanna/{google → legacy/google}/bigquery_vector.py +75 -42
  205. vanna/{google → legacy/google}/gemini_chat.py +7 -3
  206. vanna/{hf → legacy/hf}/hf.py +0 -1
  207. vanna/{milvus → legacy/milvus}/milvus_vector.py +58 -35
  208. vanna/{mock → legacy/mock}/llm.py +0 -1
  209. vanna/legacy/mock/vectordb.py +67 -0
  210. vanna/legacy/ollama/ollama.py +110 -0
  211. vanna/{openai → legacy/openai}/openai_chat.py +2 -6
  212. vanna/legacy/opensearch/opensearch_vector.py +369 -0
  213. vanna/legacy/opensearch/opensearch_vector_semantic.py +200 -0
  214. vanna/legacy/oracle/oracle_vector.py +584 -0
  215. vanna/{pgvector → legacy/pgvector}/pgvector.py +42 -13
  216. vanna/{qdrant → legacy/qdrant}/qdrant.py +2 -6
  217. vanna/legacy/qianfan/Qianfan_Chat.py +170 -0
  218. vanna/legacy/qianfan/Qianfan_embeddings.py +36 -0
  219. vanna/legacy/qianwen/QianwenAI_chat.py +132 -0
  220. vanna/{remote.py → legacy/remote.py} +28 -26
  221. vanna/{utils.py → legacy/utils.py} +6 -11
  222. vanna/{vannadb → legacy/vannadb}/vannadb_vector.py +115 -46
  223. vanna/{vllm → legacy/vllm}/vllm.py +5 -6
  224. vanna/{weaviate → legacy/weaviate}/weaviate_vector.py +59 -40
  225. vanna/{xinference → legacy/xinference}/xinference.py +6 -6
  226. vanna/py.typed +0 -0
  227. vanna/servers/__init__.py +16 -0
  228. vanna/servers/__main__.py +8 -0
  229. vanna/servers/base/__init__.py +18 -0
  230. vanna/servers/base/chat_handler.py +65 -0
  231. vanna/servers/base/models.py +111 -0
  232. vanna/servers/base/rich_chat_handler.py +141 -0
  233. vanna/servers/base/templates.py +331 -0
  234. vanna/servers/cli/__init__.py +7 -0
  235. vanna/servers/cli/server_runner.py +204 -0
  236. vanna/servers/fastapi/__init__.py +7 -0
  237. vanna/servers/fastapi/app.py +163 -0
  238. vanna/servers/fastapi/routes.py +183 -0
  239. vanna/servers/flask/__init__.py +7 -0
  240. vanna/servers/flask/app.py +132 -0
  241. vanna/servers/flask/routes.py +137 -0
  242. vanna/tools/__init__.py +41 -0
  243. vanna/tools/agent_memory.py +322 -0
  244. vanna/tools/file_system.py +879 -0
  245. vanna/tools/python.py +222 -0
  246. vanna/tools/run_sql.py +165 -0
  247. vanna/tools/visualize_data.py +195 -0
  248. vanna/utils/__init__.py +0 -0
  249. vanna/web_components/__init__.py +44 -0
  250. vanna-2.0.0.dist-info/METADATA +485 -0
  251. vanna-2.0.0.dist-info/RECORD +289 -0
  252. vanna-2.0.0.dist-info/entry_points.txt +3 -0
  253. vanna/bedrock/__init__.py +0 -1
  254. vanna/cohere/__init__.py +0 -2
  255. vanna/faiss/__init__.py +0 -1
  256. vanna/mock/vectordb.py +0 -55
  257. vanna/ollama/ollama.py +0 -103
  258. vanna/opensearch/opensearch_vector.py +0 -392
  259. vanna/opensearch/opensearch_vector_semantic.py +0 -175
  260. vanna/oracle/oracle_vector.py +0 -585
  261. vanna/qianfan/Qianfan_Chat.py +0 -165
  262. vanna/qianfan/Qianfan_embeddings.py +0 -36
  263. vanna/qianwen/QianwenAI_chat.py +0 -133
  264. vanna-0.7.9.dist-info/METADATA +0 -408
  265. vanna-0.7.9.dist-info/RECORD +0 -79
  266. /vanna/{ZhipuAI → legacy/ZhipuAI}/ZhipuAI_Chat.py +0 -0
  267. /vanna/{ZhipuAI → legacy/ZhipuAI}/__init__.py +0 -0
  268. /vanna/{anthropic → legacy/anthropic}/__init__.py +0 -0
  269. /vanna/{azuresearch → legacy/azuresearch}/__init__.py +0 -0
  270. /vanna/{base → legacy/base}/__init__.py +0 -0
  271. /vanna/{chromadb → legacy/chromadb}/__init__.py +0 -0
  272. /vanna/{deepseek → legacy/deepseek}/__init__.py +0 -0
  273. /vanna/{exceptions → legacy/exceptions}/__init__.py +0 -0
  274. /vanna/{google → legacy/google}/__init__.py +0 -0
  275. /vanna/{hf → legacy/hf}/__init__.py +0 -0
  276. /vanna/{local.py → legacy/local.py} +0 -0
  277. /vanna/{marqo → legacy/marqo}/__init__.py +0 -0
  278. /vanna/{marqo → legacy/marqo}/marqo.py +0 -0
  279. /vanna/{milvus → legacy/milvus}/__init__.py +0 -0
  280. /vanna/{mistral → legacy/mistral}/__init__.py +0 -0
  281. /vanna/{mistral → legacy/mistral}/mistral.py +0 -0
  282. /vanna/{mock → legacy/mock}/__init__.py +0 -0
  283. /vanna/{mock → legacy/mock}/embedding.py +0 -0
  284. /vanna/{ollama → legacy/ollama}/__init__.py +0 -0
  285. /vanna/{openai → legacy/openai}/__init__.py +0 -0
  286. /vanna/{openai → legacy/openai}/openai_embeddings.py +0 -0
  287. /vanna/{opensearch → legacy/opensearch}/__init__.py +0 -0
  288. /vanna/{oracle → legacy/oracle}/__init__.py +0 -0
  289. /vanna/{pgvector → legacy/pgvector}/__init__.py +0 -0
  290. /vanna/{pinecone → legacy/pinecone}/__init__.py +0 -0
  291. /vanna/{pinecone → legacy/pinecone}/pinecone_vector.py +0 -0
  292. /vanna/{qdrant → legacy/qdrant}/__init__.py +0 -0
  293. /vanna/{qianfan → legacy/qianfan}/__init__.py +0 -0
  294. /vanna/{qianwen → legacy/qianwen}/QianwenAI_embeddings.py +0 -0
  295. /vanna/{qianwen → legacy/qianwen}/__init__.py +0 -0
  296. /vanna/{types → legacy/types}/__init__.py +0 -0
  297. /vanna/{vannadb → legacy/vannadb}/__init__.py +0 -0
  298. /vanna/{vllm → legacy/vllm}/__init__.py +0 -0
  299. /vanna/{weaviate → legacy/weaviate}/__init__.py +0 -0
  300. /vanna/{xinference → legacy/xinference}/__init__.py +0 -0
  301. {vanna-0.7.9.dist-info → vanna-2.0.0.dist-info}/WHEEL +0 -0
  302. {vanna-0.7.9.dist-info → vanna-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1407 @@
1
+ """
2
+ Agent implementation for the Vanna Agents framework.
3
+
4
+ This module provides the main Agent class that orchestrates the interaction
5
+ between LLM services, tools, and conversation storage.
6
+ """
7
+
8
+ import traceback
9
+ import uuid
10
+ from typing import TYPE_CHECKING, AsyncGenerator, List, Optional
11
+
12
+ from vanna.components import (
13
+ UiComponent,
14
+ SimpleTextComponent,
15
+ RichTextComponent,
16
+ StatusBarUpdateComponent,
17
+ TaskTrackerUpdateComponent,
18
+ ChatInputUpdateComponent,
19
+ StatusCardComponent,
20
+ Task,
21
+ )
22
+ from .config import AgentConfig
23
+ from vanna.core.storage import ConversationStore
24
+ from vanna.core.llm import LlmService
25
+ from vanna.core.system_prompt import SystemPromptBuilder
26
+ from vanna.core.storage import Conversation, Message
27
+ from vanna.core.llm import LlmMessage, LlmRequest, LlmResponse
28
+ from vanna.core.tool import ToolCall, ToolContext, ToolResult, ToolSchema
29
+ from vanna.core.user import User
30
+ from vanna.core.registry import ToolRegistry
31
+ from vanna.core.system_prompt import DefaultSystemPromptBuilder
32
+ from vanna.core.lifecycle import LifecycleHook
33
+ from vanna.core.middleware import LlmMiddleware
34
+ from vanna.core.workflow import WorkflowHandler, DefaultWorkflowHandler
35
+ from vanna.core.recovery import ErrorRecoveryStrategy, RecoveryActionType
36
+ from vanna.core.enricher import ToolContextEnricher
37
+ from vanna.core.enhancer import LlmContextEnhancer, DefaultLlmContextEnhancer
38
+ from vanna.core.filter import ConversationFilter
39
+ from vanna.core.observability import ObservabilityProvider
40
+ from vanna.core.user.resolver import UserResolver
41
+ from vanna.core.user.request_context import RequestContext
42
+ from vanna.core.agent.config import UiFeature
43
+ from vanna.core.audit import AuditLogger
44
+ from vanna.capabilities.agent_memory import AgentMemory
45
+
46
+ import logging
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+ logger.info("Loaded vanna.core.agent.agent module")
51
+
52
+ if TYPE_CHECKING:
53
+ pass
54
+
55
+
56
+ class Agent:
57
+ """Main agent implementation.
58
+
59
+ The Agent class orchestrates LLM interactions, tool execution, and conversation
60
+ management. It provides 7 extensibility points for customization:
61
+
62
+ - lifecycle_hooks: Hook into message and tool execution lifecycle
63
+ - llm_middlewares: Intercept and transform LLM requests/responses
64
+ - error_recovery_strategy: Handle errors with retry logic
65
+ - context_enrichers: Add data to tool execution context
66
+ - llm_context_enhancer: Enhance LLM system prompts and messages with context
67
+ - conversation_filters: Filter conversation history before LLM calls
68
+ - observability_provider: Collect telemetry and monitoring data
69
+
70
+ Example:
71
+ agent = Agent(
72
+ llm_service=AnthropicLlmService(api_key="..."),
73
+ tool_registry=registry,
74
+ conversation_store=store,
75
+ lifecycle_hooks=[QuotaCheckHook()],
76
+ llm_middlewares=[CachingMiddleware()],
77
+ llm_context_enhancer=DefaultLlmContextEnhancer(agent_memory),
78
+ observability_provider=LoggingProvider()
79
+ )
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ llm_service: LlmService,
85
+ tool_registry: ToolRegistry,
86
+ user_resolver: UserResolver,
87
+ agent_memory: AgentMemory,
88
+ conversation_store: Optional[ConversationStore] = None,
89
+ config: AgentConfig = AgentConfig(),
90
+ system_prompt_builder: SystemPromptBuilder = DefaultSystemPromptBuilder(),
91
+ lifecycle_hooks: List[LifecycleHook] = [],
92
+ llm_middlewares: List[LlmMiddleware] = [],
93
+ workflow_handler: Optional[WorkflowHandler] = None,
94
+ error_recovery_strategy: Optional[ErrorRecoveryStrategy] = None,
95
+ context_enrichers: List[ToolContextEnricher] = [],
96
+ llm_context_enhancer: Optional[LlmContextEnhancer] = None,
97
+ conversation_filters: List[ConversationFilter] = [],
98
+ observability_provider: Optional[ObservabilityProvider] = None,
99
+ audit_logger: Optional[AuditLogger] = None,
100
+ ):
101
+ self.llm_service = llm_service
102
+ self.tool_registry = tool_registry
103
+ self.user_resolver = user_resolver
104
+ self.agent_memory = agent_memory
105
+
106
+ # Import here to avoid circular dependency
107
+ if conversation_store is None:
108
+ from vanna.integrations.local import MemoryConversationStore
109
+
110
+ conversation_store = MemoryConversationStore()
111
+
112
+ self.conversation_store = conversation_store
113
+ self.config = config
114
+ self.system_prompt_builder = system_prompt_builder
115
+ self.lifecycle_hooks = lifecycle_hooks
116
+ self.llm_middlewares = llm_middlewares
117
+
118
+ # Use DefaultWorkflowHandler if none provided
119
+ if workflow_handler is None:
120
+ workflow_handler = DefaultWorkflowHandler()
121
+ self.workflow_handler = workflow_handler
122
+
123
+ self.error_recovery_strategy = error_recovery_strategy
124
+ self.context_enrichers = context_enrichers
125
+
126
+ # Use DefaultLlmContextEnhancer if none provided
127
+ if llm_context_enhancer is None:
128
+ llm_context_enhancer = DefaultLlmContextEnhancer(agent_memory)
129
+ self.llm_context_enhancer = llm_context_enhancer
130
+
131
+ self.conversation_filters = conversation_filters
132
+ self.observability_provider = observability_provider
133
+ self.audit_logger = audit_logger
134
+
135
+ # Wire audit logger into tool registry
136
+ if self.audit_logger and self.config.audit_config.enabled:
137
+ self.tool_registry.audit_logger = self.audit_logger
138
+ self.tool_registry.audit_config = self.config.audit_config
139
+
140
+ logger.info("Initialized Agent")
141
+
142
+ async def send_message(
143
+ self,
144
+ request_context: RequestContext,
145
+ message: str,
146
+ *,
147
+ conversation_id: Optional[str] = None,
148
+ ) -> AsyncGenerator[UiComponent, None]:
149
+ """
150
+ Process a user message and yield UI components with error handling.
151
+
152
+ Args:
153
+ request_context: Request context for user resolution (includes metadata)
154
+ message: User's message content
155
+ conversation_id: Optional conversation ID; if None, creates new conversation
156
+
157
+ Yields:
158
+ UiComponent instances for UI updates
159
+ """
160
+ try:
161
+ # Delegate to internal method
162
+ async for component in self._send_message(
163
+ request_context, message, conversation_id=conversation_id
164
+ ):
165
+ yield component
166
+ except Exception as e:
167
+ # Log full stack trace
168
+ stack_trace = traceback.format_exc()
169
+ logger.error(
170
+ f"Error in send_message (conversation_id={conversation_id}): {e}\n{stack_trace}",
171
+ exc_info=True,
172
+ )
173
+
174
+ # Log to observability provider if available
175
+ if self.observability_provider:
176
+ try:
177
+ error_span = await self.observability_provider.create_span(
178
+ "agent.send_message.error",
179
+ attributes={
180
+ "error_type": type(e).__name__,
181
+ "error_message": str(e),
182
+ "conversation_id": conversation_id or "none",
183
+ },
184
+ )
185
+ await self.observability_provider.end_span(error_span)
186
+ await self.observability_provider.record_metric(
187
+ "agent.error.count",
188
+ 1.0,
189
+ "count",
190
+ tags={"error_type": type(e).__name__},
191
+ )
192
+ except Exception as obs_error:
193
+ logger.error(
194
+ f"Failed to log error to observability provider: {obs_error}",
195
+ exc_info=True,
196
+ )
197
+
198
+ # Yield error component to UI (simple, user-friendly message)
199
+ error_description = "An unexpected error occurred while processing your message. Please try again."
200
+ if conversation_id:
201
+ error_description += f"\n\nConversation ID: {conversation_id}"
202
+
203
+ yield UiComponent(
204
+ rich_component=StatusCardComponent(
205
+ title="Error Processing Message",
206
+ status="error",
207
+ description=error_description,
208
+ icon="⚠️",
209
+ ),
210
+ simple_component=SimpleTextComponent(
211
+ text=f"Error: An unexpected error occurred. Please try again.{f' (Conversation ID: {conversation_id})' if conversation_id else ''}"
212
+ ),
213
+ )
214
+
215
+ # Update status bar to show error state
216
+ yield UiComponent( # type: ignore
217
+ rich_component=StatusBarUpdateComponent(
218
+ status="error",
219
+ message="Error occurred",
220
+ detail="An unexpected error occurred while processing your message",
221
+ )
222
+ )
223
+
224
+ # Re-enable chat input so user can try again
225
+ yield UiComponent( # type: ignore
226
+ rich_component=ChatInputUpdateComponent(
227
+ placeholder="Try again...", disabled=False
228
+ )
229
+ )
230
+
231
+ async def _send_message(
232
+ self,
233
+ request_context: RequestContext,
234
+ message: str,
235
+ *,
236
+ conversation_id: Optional[str] = None,
237
+ ) -> AsyncGenerator[UiComponent, None]:
238
+ """
239
+ Internal method to process a user message and yield UI components.
240
+
241
+ Args:
242
+ request_context: Request context for user resolution (includes metadata)
243
+ message: User's message content
244
+ conversation_id: Optional conversation ID; if None, creates new conversation
245
+
246
+ Yields:
247
+ UiComponent instances for UI updates
248
+ """
249
+ # Resolve user from request context with observability
250
+ user_resolution_span = None
251
+ if self.observability_provider:
252
+ user_resolution_span = await self.observability_provider.create_span(
253
+ "agent.user_resolution",
254
+ attributes={"has_context": request_context is not None},
255
+ )
256
+
257
+ user = await self.user_resolver.resolve_user(request_context)
258
+
259
+ if self.observability_provider and user_resolution_span:
260
+ user_resolution_span.set_attribute("user_id", user.id)
261
+ await self.observability_provider.end_span(user_resolution_span)
262
+ if user_resolution_span.duration_ms():
263
+ await self.observability_provider.record_metric(
264
+ "agent.user_resolution.duration",
265
+ user_resolution_span.duration_ms() or 0,
266
+ "ms",
267
+ )
268
+
269
+ # Check if this is a starter UI request (empty message or explicit metadata flag)
270
+ is_starter_request = (not message.strip()) or request_context.metadata.get(
271
+ "starter_ui_request", False
272
+ )
273
+
274
+ if is_starter_request and self.workflow_handler:
275
+ # Handle starter UI request with observability
276
+ starter_span = None
277
+ if self.observability_provider:
278
+ starter_span = await self.observability_provider.create_span(
279
+ "agent.workflow_handler.starter_ui", attributes={"user_id": user.id}
280
+ )
281
+
282
+ try:
283
+ # Load or create conversation for context
284
+ if conversation_id is None:
285
+ conversation_id = str(uuid.uuid4())
286
+
287
+ conversation = await self.conversation_store.get_conversation(
288
+ conversation_id, user
289
+ )
290
+ if not conversation:
291
+ # Create empty conversation (will be saved if workflow produces components)
292
+ conversation = Conversation(
293
+ id=conversation_id, user=user, messages=[]
294
+ )
295
+
296
+ # Get starter UI from workflow handler
297
+ components = await self.workflow_handler.get_starter_ui(
298
+ self, user, conversation
299
+ )
300
+
301
+ if self.observability_provider and starter_span:
302
+ starter_span.set_attribute("has_components", components is not None)
303
+ starter_span.set_attribute(
304
+ "component_count", len(components) if components else 0
305
+ )
306
+
307
+ if components:
308
+ # Yield the starter UI components
309
+ for component in components:
310
+ yield component
311
+
312
+ # Yield finalization components
313
+ yield UiComponent( # type: ignore
314
+ rich_component=StatusBarUpdateComponent(
315
+ status="idle",
316
+ message="Ready",
317
+ detail="Choose an option or type a message",
318
+ )
319
+ )
320
+ yield UiComponent( # type: ignore
321
+ rich_component=ChatInputUpdateComponent(
322
+ placeholder="Ask a question...", disabled=False
323
+ )
324
+ )
325
+
326
+ if self.observability_provider and starter_span:
327
+ await self.observability_provider.end_span(starter_span)
328
+ if starter_span.duration_ms():
329
+ await self.observability_provider.record_metric(
330
+ "agent.workflow_handler.starter_ui.duration",
331
+ starter_span.duration_ms() or 0,
332
+ "ms",
333
+ )
334
+
335
+ # Save the conversation if it was newly created
336
+ if self.config.auto_save_conversations:
337
+ await self.conversation_store.update_conversation(conversation)
338
+
339
+ return # Exit without calling LLM
340
+
341
+ except Exception as e:
342
+ logger.error(f"Error generating starter UI: {e}", exc_info=True)
343
+ if self.observability_provider and starter_span:
344
+ starter_span.set_attribute("error", str(e))
345
+ await self.observability_provider.end_span(starter_span)
346
+ # Fall through to normal processing on error
347
+
348
+ # Don't process actual empty messages (that aren't starter requests)
349
+ if not message.strip():
350
+ return
351
+
352
+ # Create observability span for entire message processing
353
+ message_span = None
354
+ if self.observability_provider:
355
+ message_span = await self.observability_provider.create_span(
356
+ "agent.send_message",
357
+ attributes={
358
+ "user_id": user.id,
359
+ "conversation_id": conversation_id or "new",
360
+ },
361
+ )
362
+
363
+ # Run before_message hooks with observability
364
+ modified_message = message
365
+ for hook in self.lifecycle_hooks:
366
+ hook_span = None
367
+ if self.observability_provider:
368
+ hook_span = await self.observability_provider.create_span(
369
+ "agent.hook.before_message",
370
+ attributes={"hook": hook.__class__.__name__},
371
+ )
372
+
373
+ hook_result = await hook.before_message(user, modified_message)
374
+ if hook_result is not None:
375
+ modified_message = hook_result
376
+
377
+ if self.observability_provider and hook_span:
378
+ hook_span.set_attribute("modified_message", hook_result is not None)
379
+ await self.observability_provider.end_span(hook_span)
380
+ if hook_span.duration_ms():
381
+ await self.observability_provider.record_metric(
382
+ "agent.hook.duration",
383
+ hook_span.duration_ms() or 0,
384
+ "ms",
385
+ tags={
386
+ "hook": hook.__class__.__name__,
387
+ "phase": "before_message",
388
+ },
389
+ )
390
+
391
+ # Use the potentially modified message
392
+ message = modified_message
393
+
394
+ # Generate conversation ID and request ID if not provided
395
+ if conversation_id is None:
396
+ conversation_id = str(uuid.uuid4())
397
+
398
+ request_id = str(uuid.uuid4())
399
+
400
+ # Update status to working
401
+ yield UiComponent( # type: ignore
402
+ rich_component=StatusBarUpdateComponent(
403
+ status="working",
404
+ message="Processing your request...",
405
+ detail="Analyzing query",
406
+ )
407
+ )
408
+
409
+ # Load or create conversation with observability (but don't add message yet)
410
+ conversation_span = None
411
+ if self.observability_provider:
412
+ conversation_span = await self.observability_provider.create_span(
413
+ "agent.conversation.load",
414
+ attributes={"conversation_id": conversation_id, "user_id": user.id},
415
+ )
416
+
417
+ conversation = await self.conversation_store.get_conversation(
418
+ conversation_id, user
419
+ )
420
+
421
+ is_new_conversation = conversation is None
422
+
423
+ if not conversation:
424
+ # Create empty conversation (will add message after workflow handler check)
425
+ conversation = Conversation(id=conversation_id, user=user, messages=[])
426
+
427
+ if self.observability_provider and conversation_span:
428
+ conversation_span.set_attribute("is_new", is_new_conversation)
429
+ conversation_span.set_attribute("message_count", len(conversation.messages))
430
+ await self.observability_provider.end_span(conversation_span)
431
+ if conversation_span.duration_ms():
432
+ await self.observability_provider.record_metric(
433
+ "agent.conversation.load.duration",
434
+ conversation_span.duration_ms() or 0,
435
+ "ms",
436
+ tags={"is_new": str(is_new_conversation)},
437
+ )
438
+
439
+ # Try workflow handler before adding message to conversation
440
+ if self.workflow_handler:
441
+ trigger_span = None
442
+ if self.observability_provider:
443
+ trigger_span = await self.observability_provider.create_span(
444
+ "agent.workflow_handler.try_handle",
445
+ attributes={"user_id": user.id, "conversation_id": conversation_id},
446
+ )
447
+
448
+ try:
449
+ workflow_result = await self.workflow_handler.try_handle(
450
+ self, user, conversation, message
451
+ )
452
+
453
+ if self.observability_provider and trigger_span:
454
+ trigger_span.set_attribute(
455
+ "should_skip_llm", workflow_result.should_skip_llm
456
+ )
457
+
458
+ if workflow_result.should_skip_llm:
459
+ # Workflow handled the message, short-circuit LLM
460
+
461
+ # Apply conversation mutation if provided
462
+ if workflow_result.conversation_mutation:
463
+ await workflow_result.conversation_mutation(conversation)
464
+
465
+ # Stream components
466
+ if workflow_result.components:
467
+ if isinstance(workflow_result.components, list):
468
+ for component in workflow_result.components:
469
+ yield component
470
+ else:
471
+ # AsyncGenerator
472
+ async for component in workflow_result.components:
473
+ yield component
474
+
475
+ # Finalize response (status bar + chat input)
476
+ yield UiComponent( # type: ignore
477
+ rich_component=StatusBarUpdateComponent(
478
+ status="idle",
479
+ message="Workflow complete",
480
+ detail="Ready for next message",
481
+ )
482
+ )
483
+ yield UiComponent( # type: ignore
484
+ rich_component=ChatInputUpdateComponent(
485
+ placeholder="Ask a question...", disabled=False
486
+ )
487
+ )
488
+
489
+ # Save conversation if auto-save enabled
490
+ if self.config.auto_save_conversations:
491
+ await self.conversation_store.update_conversation(conversation)
492
+
493
+ if self.observability_provider and trigger_span:
494
+ await self.observability_provider.end_span(trigger_span)
495
+
496
+ # Exit without calling LLM
497
+ return
498
+
499
+ except Exception as e:
500
+ logger.error(f"Error in workflow handler: {e}", exc_info=True)
501
+ if self.observability_provider and trigger_span:
502
+ trigger_span.set_attribute("error", str(e))
503
+ await self.observability_provider.end_span(trigger_span)
504
+ # Fall through to normal LLM processing on error
505
+
506
+ finally:
507
+ if self.observability_provider and trigger_span:
508
+ await self.observability_provider.end_span(trigger_span)
509
+
510
+ # Persist new conversation to store before adding message
511
+ if is_new_conversation:
512
+ await self.conversation_store.update_conversation(conversation)
513
+
514
+ # Not triggered, add user message to conversation now
515
+ conversation.add_message(Message(role="user", content=message))
516
+
517
+ # Add initial task
518
+ context_task = Task(
519
+ title="Load conversation context",
520
+ description="Reading message history and user context",
521
+ status="pending",
522
+ )
523
+ yield UiComponent( # type: ignore
524
+ rich_component=TaskTrackerUpdateComponent.add_task(context_task)
525
+ )
526
+
527
+ # Collect available UI features for auditing
528
+ ui_features_available = []
529
+ for feature_name in self.config.ui_features.feature_group_access.keys():
530
+ if self.config.ui_features.can_user_access_feature(feature_name, user):
531
+ ui_features_available.append(feature_name)
532
+
533
+ # Create context with observability provider and UI features
534
+ context = ToolContext(
535
+ user=user,
536
+ conversation_id=conversation_id,
537
+ request_id=request_id,
538
+ agent_memory=self.agent_memory,
539
+ observability_provider=self.observability_provider,
540
+ metadata={"ui_features_available": ui_features_available},
541
+ )
542
+
543
+ # Enrich context with additional data with observability
544
+ for enricher in self.context_enrichers:
545
+ enrichment_span = None
546
+ if self.observability_provider:
547
+ enrichment_span = await self.observability_provider.create_span(
548
+ "agent.context.enrichment",
549
+ attributes={"enricher": enricher.__class__.__name__},
550
+ )
551
+
552
+ context = await enricher.enrich_context(context)
553
+
554
+ if self.observability_provider and enrichment_span:
555
+ await self.observability_provider.end_span(enrichment_span)
556
+ if enrichment_span.duration_ms():
557
+ await self.observability_provider.record_metric(
558
+ "agent.enrichment.duration",
559
+ enrichment_span.duration_ms() or 0,
560
+ "ms",
561
+ tags={"enricher": enricher.__class__.__name__},
562
+ )
563
+
564
+ # Get available tools for user with observability
565
+ schema_span = None
566
+ if self.observability_provider:
567
+ schema_span = await self.observability_provider.create_span(
568
+ "agent.tool_schemas.fetch", attributes={"user_id": user.id}
569
+ )
570
+
571
+ tool_schemas = await self.tool_registry.get_schemas(user)
572
+
573
+ if self.observability_provider and schema_span:
574
+ schema_span.set_attribute("schema_count", len(tool_schemas))
575
+ await self.observability_provider.end_span(schema_span)
576
+ if schema_span.duration_ms():
577
+ await self.observability_provider.record_metric(
578
+ "agent.tool_schemas.duration",
579
+ schema_span.duration_ms() or 0,
580
+ "ms",
581
+ tags={"schema_count": str(len(tool_schemas))},
582
+ )
583
+
584
+ # Update task status to completed
585
+ yield UiComponent( # type: ignore
586
+ rich_component=TaskTrackerUpdateComponent.update_task(
587
+ context_task.id, status="completed"
588
+ )
589
+ )
590
+
591
+ # Build system prompt with observability
592
+ prompt_span = None
593
+ if self.observability_provider:
594
+ prompt_span = await self.observability_provider.create_span(
595
+ "agent.system_prompt.build",
596
+ attributes={"tool_count": len(tool_schemas)},
597
+ )
598
+
599
+ system_prompt = await self.system_prompt_builder.build_system_prompt(
600
+ user, tool_schemas
601
+ )
602
+
603
+ # Enhance system prompt with LLM context enhancer
604
+ if self.llm_context_enhancer and system_prompt is not None:
605
+ enhancement_span = None
606
+ if self.observability_provider:
607
+ enhancement_span = await self.observability_provider.create_span(
608
+ "agent.llm_context.enhance_system_prompt",
609
+ attributes={
610
+ "enhancer": self.llm_context_enhancer.__class__.__name__
611
+ },
612
+ )
613
+
614
+ system_prompt = await self.llm_context_enhancer.enhance_system_prompt(
615
+ system_prompt, message, user
616
+ )
617
+
618
+ if self.observability_provider and enhancement_span:
619
+ await self.observability_provider.end_span(enhancement_span)
620
+ if enhancement_span.duration_ms():
621
+ await self.observability_provider.record_metric(
622
+ "agent.llm_context.enhance_system_prompt.duration",
623
+ enhancement_span.duration_ms() or 0,
624
+ "ms",
625
+ tags={"enhancer": self.llm_context_enhancer.__class__.__name__},
626
+ )
627
+
628
+ if self.observability_provider and prompt_span:
629
+ prompt_span.set_attribute(
630
+ "prompt_length", len(system_prompt) if system_prompt else 0
631
+ )
632
+ await self.observability_provider.end_span(prompt_span)
633
+ if prompt_span.duration_ms():
634
+ await self.observability_provider.record_metric(
635
+ "agent.system_prompt.duration", prompt_span.duration_ms() or 0, "ms"
636
+ )
637
+
638
+ # Build LLM request
639
+ request = await self._build_llm_request(
640
+ conversation, tool_schemas, user, system_prompt
641
+ )
642
+
643
+ # Process with tool loop
644
+ tool_iterations = 0
645
+
646
+ while tool_iterations < self.config.max_tool_iterations:
647
+ if self.config.include_thinking_indicators and tool_iterations == 0:
648
+ # TODO: Yield thinking indicator
649
+ pass
650
+
651
+ # Get LLM response
652
+ if self.config.stream_responses:
653
+ response = await self._handle_streaming_response(request)
654
+ else:
655
+ response = await self._send_llm_request(request)
656
+
657
+ # Handle tool calls
658
+ if response.is_tool_call():
659
+ tool_iterations += 1
660
+
661
+ # First, add the assistant message with tool_calls to the conversation
662
+ # This is required for OpenAI API - tool messages must follow assistant messages with tool_calls
663
+ assistant_message = Message(
664
+ role="assistant",
665
+ content=response.content or "", # Ensure content is not None
666
+ tool_calls=response.tool_calls,
667
+ )
668
+ conversation.add_message(assistant_message)
669
+
670
+ if response.content is not None:
671
+ # Yield any partial content from the assistant before tool execution
672
+ has_tool_invocation_message_in_chat = (
673
+ self.config.ui_features.can_user_access_feature(
674
+ UiFeature.UI_FEATURE_SHOW_TOOL_INVOCATION_MESSAGE_IN_CHAT,
675
+ user,
676
+ )
677
+ )
678
+ if has_tool_invocation_message_in_chat:
679
+ yield UiComponent(
680
+ rich_component=RichTextComponent(
681
+ content=response.content, markdown=True
682
+ ),
683
+ simple_component=SimpleTextComponent(text=response.content),
684
+ )
685
+
686
+ # Update status to executing tools
687
+ yield UiComponent( # type: ignore
688
+ rich_component=StatusBarUpdateComponent(
689
+ status="working",
690
+ message="Executing tools...",
691
+ detail=f"Running {len(response.tool_calls or [])} tools",
692
+ )
693
+ )
694
+ else:
695
+ # Yield as a status update instead
696
+ yield UiComponent( # type: ignore
697
+ rich_component=StatusBarUpdateComponent(
698
+ status="working", message=response.content, detail=""
699
+ )
700
+ )
701
+
702
+ # Collect all tool results first
703
+ tool_results = []
704
+ for i, tool_call in enumerate(response.tool_calls or []):
705
+ # Add task for this tool execution
706
+ tool_task = Task(
707
+ title=f"Execute {tool_call.name}",
708
+ description=f"Running tool with provided arguments",
709
+ status="in_progress",
710
+ )
711
+
712
+ has_tool_names_access = (
713
+ self.config.ui_features.can_user_access_feature(
714
+ UiFeature.UI_FEATURE_SHOW_TOOL_NAMES, user
715
+ )
716
+ )
717
+
718
+ # Audit UI feature access check
719
+ if (
720
+ self.audit_logger
721
+ and self.config.audit_config.enabled
722
+ and self.config.audit_config.log_ui_feature_checks
723
+ ):
724
+ await self.audit_logger.log_ui_feature_access(
725
+ user=user,
726
+ feature_name=UiFeature.UI_FEATURE_SHOW_TOOL_NAMES,
727
+ access_granted=has_tool_names_access,
728
+ required_groups=self.config.ui_features.feature_group_access.get(
729
+ UiFeature.UI_FEATURE_SHOW_TOOL_NAMES, []
730
+ ),
731
+ conversation_id=conversation.id,
732
+ request_id=request_id,
733
+ )
734
+
735
+ if has_tool_names_access:
736
+ yield UiComponent( # type: ignore
737
+ rich_component=TaskTrackerUpdateComponent.add_task(
738
+ tool_task
739
+ )
740
+ )
741
+
742
+ response_str = response.content
743
+
744
+ # Use primitive StatusCard instead of semantic ToolExecutionComponent
745
+ tool_status_card = StatusCardComponent(
746
+ title=f"Executing {tool_call.name}",
747
+ status="running",
748
+ description=f"Running tool with {len(tool_call.arguments)} arguments",
749
+ icon="⚙️",
750
+ metadata=tool_call.arguments,
751
+ )
752
+
753
+ has_tool_args_access = (
754
+ self.config.ui_features.can_user_access_feature(
755
+ UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS, user
756
+ )
757
+ )
758
+
759
+ # Audit UI feature access check
760
+ if (
761
+ self.audit_logger
762
+ and self.config.audit_config.enabled
763
+ and self.config.audit_config.log_ui_feature_checks
764
+ ):
765
+ await self.audit_logger.log_ui_feature_access(
766
+ user=user,
767
+ feature_name=UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS,
768
+ access_granted=has_tool_args_access,
769
+ required_groups=self.config.ui_features.feature_group_access.get(
770
+ UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS, []
771
+ ),
772
+ conversation_id=conversation.id,
773
+ request_id=request_id,
774
+ )
775
+
776
+ if has_tool_args_access:
777
+ yield UiComponent(
778
+ rich_component=tool_status_card,
779
+ simple_component=SimpleTextComponent(
780
+ text=response_str or ""
781
+ ),
782
+ )
783
+
784
+ # Run before_tool hooks with observability
785
+ tool = await self.tool_registry.get_tool(tool_call.name)
786
+ if tool:
787
+ for hook in self.lifecycle_hooks:
788
+ hook_span = None
789
+ if self.observability_provider:
790
+ hook_span = (
791
+ await self.observability_provider.create_span(
792
+ "agent.hook.before_tool",
793
+ attributes={
794
+ "hook": hook.__class__.__name__,
795
+ "tool": tool_call.name,
796
+ },
797
+ )
798
+ )
799
+
800
+ await hook.before_tool(tool, context)
801
+
802
+ if self.observability_provider and hook_span:
803
+ await self.observability_provider.end_span(hook_span)
804
+ if hook_span.duration_ms():
805
+ await self.observability_provider.record_metric(
806
+ "agent.hook.duration",
807
+ hook_span.duration_ms() or 0,
808
+ "ms",
809
+ tags={
810
+ "hook": hook.__class__.__name__,
811
+ "phase": "before_tool",
812
+ "tool": tool_call.name,
813
+ },
814
+ )
815
+
816
+ # Execute tool with observability
817
+ tool_exec_span = None
818
+ if self.observability_provider:
819
+ tool_exec_span = await self.observability_provider.create_span(
820
+ "agent.tool.execute",
821
+ attributes={
822
+ "tool": tool_call.name,
823
+ "arg_count": len(tool_call.arguments),
824
+ },
825
+ )
826
+
827
+ result = await self.tool_registry.execute(tool_call, context)
828
+
829
+ if self.observability_provider and tool_exec_span:
830
+ tool_exec_span.set_attribute("success", result.success)
831
+ if not result.success:
832
+ tool_exec_span.set_attribute(
833
+ "error", result.error or "unknown"
834
+ )
835
+ await self.observability_provider.end_span(tool_exec_span)
836
+ if tool_exec_span.duration_ms():
837
+ await self.observability_provider.record_metric(
838
+ "agent.tool.duration",
839
+ tool_exec_span.duration_ms() or 0,
840
+ "ms",
841
+ tags={
842
+ "tool": tool_call.name,
843
+ "success": str(result.success),
844
+ },
845
+ )
846
+
847
+ # Run after_tool hooks with observability
848
+ for hook in self.lifecycle_hooks:
849
+ hook_span = None
850
+ if self.observability_provider:
851
+ hook_span = await self.observability_provider.create_span(
852
+ "agent.hook.after_tool",
853
+ attributes={
854
+ "hook": hook.__class__.__name__,
855
+ "tool": tool_call.name,
856
+ },
857
+ )
858
+
859
+ modified_result = await hook.after_tool(result)
860
+ if modified_result is not None:
861
+ result = modified_result
862
+
863
+ if self.observability_provider and hook_span:
864
+ hook_span.set_attribute(
865
+ "modified_result", modified_result is not None
866
+ )
867
+ await self.observability_provider.end_span(hook_span)
868
+ if hook_span.duration_ms():
869
+ await self.observability_provider.record_metric(
870
+ "agent.hook.duration",
871
+ hook_span.duration_ms() or 0,
872
+ "ms",
873
+ tags={
874
+ "hook": hook.__class__.__name__,
875
+ "phase": "after_tool",
876
+ "tool": tool_call.name,
877
+ },
878
+ )
879
+
880
+ # Update status card to show completion
881
+ final_status = "success" if result.success else "error"
882
+ final_description = (
883
+ f"Tool completed successfully"
884
+ if result.success
885
+ else f"Tool failed: {result.error or 'Unknown error'}"
886
+ )
887
+
888
+ has_tool_args_access_2 = (
889
+ self.config.ui_features.can_user_access_feature(
890
+ UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS, user
891
+ )
892
+ )
893
+
894
+ # Audit UI feature access check
895
+ if (
896
+ self.audit_logger
897
+ and self.config.audit_config.enabled
898
+ and self.config.audit_config.log_ui_feature_checks
899
+ ):
900
+ await self.audit_logger.log_ui_feature_access(
901
+ user=user,
902
+ feature_name=UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS,
903
+ access_granted=has_tool_args_access_2,
904
+ required_groups=self.config.ui_features.feature_group_access.get(
905
+ UiFeature.UI_FEATURE_SHOW_TOOL_ARGUMENTS, []
906
+ ),
907
+ conversation_id=conversation.id,
908
+ request_id=request_id,
909
+ )
910
+
911
+ if has_tool_args_access_2:
912
+ yield UiComponent(
913
+ rich_component=tool_status_card.set_status(
914
+ final_status, final_description
915
+ ),
916
+ simple_component=SimpleTextComponent(
917
+ text=final_description
918
+ ),
919
+ )
920
+
921
+ has_tool_names_access_2 = (
922
+ self.config.ui_features.can_user_access_feature(
923
+ UiFeature.UI_FEATURE_SHOW_TOOL_NAMES, user
924
+ )
925
+ )
926
+
927
+ # Audit UI feature access check
928
+ if (
929
+ self.audit_logger
930
+ and self.config.audit_config.enabled
931
+ and self.config.audit_config.log_ui_feature_checks
932
+ ):
933
+ await self.audit_logger.log_ui_feature_access(
934
+ user=user,
935
+ feature_name=UiFeature.UI_FEATURE_SHOW_TOOL_NAMES,
936
+ access_granted=has_tool_names_access_2,
937
+ required_groups=self.config.ui_features.feature_group_access.get(
938
+ UiFeature.UI_FEATURE_SHOW_TOOL_NAMES, []
939
+ ),
940
+ conversation_id=conversation.id,
941
+ request_id=request_id,
942
+ )
943
+
944
+ if has_tool_names_access_2:
945
+ # Update tool task to completed
946
+ yield UiComponent( # type: ignore
947
+ rich_component=TaskTrackerUpdateComponent.update_task(
948
+ tool_task.id,
949
+ status="completed",
950
+ detail=f"Tool {'completed successfully' if result.success else 'return an error'}",
951
+ )
952
+ )
953
+
954
+ # Yield tool result
955
+ if result.ui_component:
956
+ # For errors, check if user has access to see error details
957
+ if not result.success:
958
+ has_tool_error_access = (
959
+ self.config.ui_features.can_user_access_feature(
960
+ UiFeature.UI_FEATURE_SHOW_TOOL_ERROR, user
961
+ )
962
+ )
963
+
964
+ # Audit UI feature access check
965
+ if (
966
+ self.audit_logger
967
+ and self.config.audit_config.enabled
968
+ and self.config.audit_config.log_ui_feature_checks
969
+ ):
970
+ await self.audit_logger.log_ui_feature_access(
971
+ user=user,
972
+ feature_name=UiFeature.UI_FEATURE_SHOW_TOOL_ERROR,
973
+ access_granted=has_tool_error_access,
974
+ required_groups=self.config.ui_features.feature_group_access.get(
975
+ UiFeature.UI_FEATURE_SHOW_TOOL_ERROR, []
976
+ ),
977
+ conversation_id=conversation.id,
978
+ request_id=request_id,
979
+ )
980
+
981
+ if has_tool_error_access:
982
+ yield result.ui_component
983
+ else:
984
+ # Success results are always shown if they exist
985
+ yield result.ui_component
986
+
987
+ # Collect tool result data
988
+ tool_results.append(
989
+ {
990
+ "tool_call_id": tool_call.id,
991
+ "content": (
992
+ result.result_for_llm
993
+ if result.success
994
+ else result.error or "Tool execution failed"
995
+ ),
996
+ }
997
+ )
998
+
999
+ # Add tool responses to conversation
1000
+ # For APIs that need all tool results in one message, this helps
1001
+ for tool_result in tool_results:
1002
+ tool_response_message = Message(
1003
+ role="tool",
1004
+ content=tool_result["content"],
1005
+ tool_call_id=tool_result["tool_call_id"],
1006
+ )
1007
+ conversation.add_message(tool_response_message)
1008
+
1009
+ # Rebuild request with tool responses
1010
+ request = await self._build_llm_request(
1011
+ conversation, tool_schemas, user, system_prompt
1012
+ )
1013
+ else:
1014
+ # Update status to idle and set completion message
1015
+ yield UiComponent( # type: ignore
1016
+ rich_component=StatusBarUpdateComponent(
1017
+ status="idle",
1018
+ message="Response complete",
1019
+ detail="Ready for next message",
1020
+ )
1021
+ )
1022
+
1023
+ # Update chat input placeholder
1024
+ yield UiComponent( # type: ignore
1025
+ rich_component=ChatInputUpdateComponent(
1026
+ placeholder="Ask a follow-up question...", disabled=False
1027
+ )
1028
+ )
1029
+
1030
+ # Yield final text response
1031
+ if response.content:
1032
+ # Add assistant response to conversation
1033
+ conversation.add_message(
1034
+ Message(role="assistant", content=response.content)
1035
+ )
1036
+ yield UiComponent(
1037
+ rich_component=RichTextComponent(
1038
+ content=response.content, markdown=True
1039
+ ),
1040
+ simple_component=SimpleTextComponent(text=response.content),
1041
+ )
1042
+ break
1043
+
1044
+ # Check if we hit the tool iteration limit
1045
+ if tool_iterations >= self.config.max_tool_iterations:
1046
+ # The loop exited due to hitting the limit, not due to a natural completion
1047
+ logger.warning(
1048
+ f"Tool iteration limit reached: {tool_iterations}/{self.config.max_tool_iterations}"
1049
+ )
1050
+
1051
+ # Update status bar to show warning
1052
+ yield UiComponent( # type: ignore
1053
+ rich_component=StatusBarUpdateComponent(
1054
+ status="warning",
1055
+ message="Tool limit reached",
1056
+ detail=f"Stopped after {tool_iterations} tool executions. The task may be incomplete.",
1057
+ )
1058
+ )
1059
+
1060
+ # Provide detailed warning message to user
1061
+ warning_message = f"""⚠️ **Tool Execution Limit Reached**
1062
+
1063
+ The agent stopped after executing {tool_iterations} tools (the configured maximum). The task may not be fully complete.
1064
+
1065
+ You can:
1066
+ - Ask me to continue where I left off
1067
+ - Adjust the `max_tool_iterations` setting if you need more tool calls
1068
+ - Break the task into smaller steps"""
1069
+
1070
+ yield UiComponent(
1071
+ rich_component=RichTextComponent(
1072
+ content=warning_message, markdown=True
1073
+ ),
1074
+ simple_component=SimpleTextComponent(
1075
+ text=f"Tool limit reached after {tool_iterations} executions. Task may be incomplete."
1076
+ ),
1077
+ )
1078
+
1079
+ # Update chat input to suggest follow-up
1080
+ yield UiComponent( # type: ignore
1081
+ rich_component=ChatInputUpdateComponent(
1082
+ placeholder="Continue the task or ask me something else...",
1083
+ disabled=False,
1084
+ )
1085
+ )
1086
+
1087
+ # Save conversation if configured
1088
+ if self.config.auto_save_conversations:
1089
+ save_span = None
1090
+ if self.observability_provider:
1091
+ save_span = await self.observability_provider.create_span(
1092
+ "agent.conversation.save",
1093
+ attributes={
1094
+ "conversation_id": conversation_id,
1095
+ "message_count": len(conversation.messages),
1096
+ },
1097
+ )
1098
+
1099
+ await self.conversation_store.update_conversation(conversation)
1100
+
1101
+ if self.observability_provider and save_span:
1102
+ await self.observability_provider.end_span(save_span)
1103
+ if save_span.duration_ms():
1104
+ await self.observability_provider.record_metric(
1105
+ "agent.conversation.save.duration",
1106
+ save_span.duration_ms() or 0,
1107
+ "ms",
1108
+ )
1109
+
1110
+ # Run after_message hooks with observability
1111
+ for hook in self.lifecycle_hooks:
1112
+ hook_span = None
1113
+ if self.observability_provider:
1114
+ hook_span = await self.observability_provider.create_span(
1115
+ "agent.hook.after_message",
1116
+ attributes={"hook": hook.__class__.__name__},
1117
+ )
1118
+
1119
+ await hook.after_message(conversation)
1120
+
1121
+ if self.observability_provider and hook_span:
1122
+ await self.observability_provider.end_span(hook_span)
1123
+ if hook_span.duration_ms():
1124
+ await self.observability_provider.record_metric(
1125
+ "agent.hook.duration",
1126
+ hook_span.duration_ms() or 0,
1127
+ "ms",
1128
+ tags={
1129
+ "hook": hook.__class__.__name__,
1130
+ "phase": "after_message",
1131
+ },
1132
+ )
1133
+
1134
+ # End observability span and record metrics
1135
+ if self.observability_provider and message_span:
1136
+ message_span.set_attribute("tool_iterations", tool_iterations)
1137
+
1138
+ # Track if we hit the tool iteration limit
1139
+ hit_tool_limit = tool_iterations >= self.config.max_tool_iterations
1140
+ message_span.set_attribute("hit_tool_limit", hit_tool_limit)
1141
+ if hit_tool_limit:
1142
+ message_span.set_attribute("incomplete_response", True)
1143
+ logger.info(
1144
+ f"Tool limit reached - marking response as potentially incomplete"
1145
+ )
1146
+
1147
+ await self.observability_provider.end_span(message_span)
1148
+ if message_span.duration_ms():
1149
+ await self.observability_provider.record_metric(
1150
+ "agent.message.duration",
1151
+ message_span.duration_ms() or 0,
1152
+ "ms",
1153
+ tags={"user_id": user.id, "hit_tool_limit": str(hit_tool_limit)},
1154
+ )
1155
+
1156
+ async def get_available_tools(self, user: User) -> List[ToolSchema]:
1157
+ """Get tools available to the user."""
1158
+ return await self.tool_registry.get_schemas(user)
1159
+
1160
+ async def _build_llm_request(
1161
+ self,
1162
+ conversation: Conversation,
1163
+ tool_schemas: List[ToolSchema],
1164
+ user: User,
1165
+ system_prompt: Optional[str] = None,
1166
+ ) -> LlmRequest:
1167
+ """Build LLM request from conversation and tools."""
1168
+ # Apply conversation filters with observability
1169
+ filtered_messages = conversation.messages
1170
+ for filter in self.conversation_filters:
1171
+ filter_span = None
1172
+ if self.observability_provider:
1173
+ filter_span = await self.observability_provider.create_span(
1174
+ "agent.conversation.filter",
1175
+ attributes={
1176
+ "filter": filter.__class__.__name__,
1177
+ "message_count_before": len(filtered_messages),
1178
+ },
1179
+ )
1180
+
1181
+ filtered_messages = await filter.filter_messages(filtered_messages)
1182
+
1183
+ if self.observability_provider and filter_span:
1184
+ filter_span.set_attribute("message_count_after", len(filtered_messages))
1185
+ await self.observability_provider.end_span(filter_span)
1186
+ if filter_span.duration_ms():
1187
+ await self.observability_provider.record_metric(
1188
+ "agent.filter.duration",
1189
+ filter_span.duration_ms() or 0,
1190
+ "ms",
1191
+ tags={"filter": filter.__class__.__name__},
1192
+ )
1193
+
1194
+ messages = []
1195
+ for msg in filtered_messages:
1196
+ llm_msg = LlmMessage(
1197
+ role=msg.role,
1198
+ content=msg.content,
1199
+ tool_calls=msg.tool_calls,
1200
+ tool_call_id=msg.tool_call_id,
1201
+ )
1202
+ messages.append(llm_msg)
1203
+
1204
+ # Enhance messages with LLM context enhancer
1205
+ if self.llm_context_enhancer:
1206
+ enhancement_span = None
1207
+ if self.observability_provider:
1208
+ enhancement_span = await self.observability_provider.create_span(
1209
+ "agent.llm_context.enhance_user_messages",
1210
+ attributes={
1211
+ "enhancer": self.llm_context_enhancer.__class__.__name__,
1212
+ "message_count": len(messages),
1213
+ },
1214
+ )
1215
+
1216
+ messages = await self.llm_context_enhancer.enhance_user_messages(
1217
+ messages, user
1218
+ )
1219
+
1220
+ if self.observability_provider and enhancement_span:
1221
+ enhancement_span.set_attribute("message_count_after", len(messages))
1222
+ await self.observability_provider.end_span(enhancement_span)
1223
+ if enhancement_span.duration_ms():
1224
+ await self.observability_provider.record_metric(
1225
+ "agent.llm_context.enhance_user_messages.duration",
1226
+ enhancement_span.duration_ms() or 0,
1227
+ "ms",
1228
+ tags={"enhancer": self.llm_context_enhancer.__class__.__name__},
1229
+ )
1230
+
1231
+ return LlmRequest(
1232
+ messages=messages,
1233
+ tools=tool_schemas if tool_schemas else None,
1234
+ user=user,
1235
+ temperature=self.config.temperature,
1236
+ max_tokens=self.config.max_tokens,
1237
+ stream=self.config.stream_responses,
1238
+ system_prompt=system_prompt,
1239
+ )
1240
+
1241
+ async def _send_llm_request(self, request: LlmRequest) -> LlmResponse:
1242
+ """Send LLM request with middleware and observability."""
1243
+ # Apply before_llm_request middlewares with observability
1244
+ for middleware in self.llm_middlewares:
1245
+ mw_span = None
1246
+ if self.observability_provider:
1247
+ mw_span = await self.observability_provider.create_span(
1248
+ "agent.middleware.before_llm",
1249
+ attributes={"middleware": middleware.__class__.__name__},
1250
+ )
1251
+
1252
+ request = await middleware.before_llm_request(request)
1253
+
1254
+ if self.observability_provider and mw_span:
1255
+ await self.observability_provider.end_span(mw_span)
1256
+ if mw_span.duration_ms():
1257
+ await self.observability_provider.record_metric(
1258
+ "agent.middleware.duration",
1259
+ mw_span.duration_ms() or 0,
1260
+ "ms",
1261
+ tags={
1262
+ "middleware": middleware.__class__.__name__,
1263
+ "phase": "before_llm",
1264
+ },
1265
+ )
1266
+
1267
+ # Create observability span for LLM call
1268
+ llm_span = None
1269
+ if self.observability_provider:
1270
+ llm_span = await self.observability_provider.create_span(
1271
+ "llm.request",
1272
+ attributes={
1273
+ "model": getattr(self.llm_service, "model", "unknown"),
1274
+ "stream": request.stream,
1275
+ },
1276
+ )
1277
+
1278
+ # Send request
1279
+ response = await self.llm_service.send_request(request)
1280
+
1281
+ # End span and record metrics
1282
+ if self.observability_provider and llm_span:
1283
+ await self.observability_provider.end_span(llm_span)
1284
+ if llm_span.duration_ms():
1285
+ await self.observability_provider.record_metric(
1286
+ "llm.request.duration", llm_span.duration_ms() or 0, "ms"
1287
+ )
1288
+
1289
+ # Apply after_llm_response middlewares with observability
1290
+ for middleware in self.llm_middlewares:
1291
+ mw_span = None
1292
+ if self.observability_provider:
1293
+ mw_span = await self.observability_provider.create_span(
1294
+ "agent.middleware.after_llm",
1295
+ attributes={"middleware": middleware.__class__.__name__},
1296
+ )
1297
+
1298
+ response = await middleware.after_llm_response(request, response)
1299
+
1300
+ if self.observability_provider and mw_span:
1301
+ await self.observability_provider.end_span(mw_span)
1302
+ if mw_span.duration_ms():
1303
+ await self.observability_provider.record_metric(
1304
+ "agent.middleware.duration",
1305
+ mw_span.duration_ms() or 0,
1306
+ "ms",
1307
+ tags={
1308
+ "middleware": middleware.__class__.__name__,
1309
+ "phase": "after_llm",
1310
+ },
1311
+ )
1312
+
1313
+ return response
1314
+
1315
+ async def _handle_streaming_response(self, request: LlmRequest) -> LlmResponse:
1316
+ """Handle streaming response from LLM."""
1317
+ # Apply before_llm_request middlewares with observability
1318
+ for middleware in self.llm_middlewares:
1319
+ mw_span = None
1320
+ if self.observability_provider:
1321
+ mw_span = await self.observability_provider.create_span(
1322
+ "agent.middleware.before_llm",
1323
+ attributes={
1324
+ "middleware": middleware.__class__.__name__,
1325
+ "stream": True,
1326
+ },
1327
+ )
1328
+
1329
+ request = await middleware.before_llm_request(request)
1330
+
1331
+ if self.observability_provider and mw_span:
1332
+ await self.observability_provider.end_span(mw_span)
1333
+ if mw_span.duration_ms():
1334
+ await self.observability_provider.record_metric(
1335
+ "agent.middleware.duration",
1336
+ mw_span.duration_ms() or 0,
1337
+ "ms",
1338
+ tags={
1339
+ "middleware": middleware.__class__.__name__,
1340
+ "phase": "before_llm",
1341
+ "stream": "true",
1342
+ },
1343
+ )
1344
+
1345
+ accumulated_content = ""
1346
+ accumulated_tool_calls = []
1347
+
1348
+ # Create span for streaming
1349
+ stream_span = None
1350
+ if self.observability_provider:
1351
+ stream_span = await self.observability_provider.create_span(
1352
+ "llm.stream",
1353
+ attributes={"model": getattr(self.llm_service, "model", "unknown")},
1354
+ )
1355
+
1356
+ async for chunk in self.llm_service.stream_request(request):
1357
+ if chunk.content:
1358
+ accumulated_content += chunk.content
1359
+ # Could yield intermediate TextChunk here
1360
+
1361
+ if chunk.tool_calls:
1362
+ accumulated_tool_calls.extend(chunk.tool_calls)
1363
+
1364
+ # End streaming span
1365
+ if self.observability_provider and stream_span:
1366
+ stream_span.set_attribute("content_length", len(accumulated_content))
1367
+ stream_span.set_attribute("tool_call_count", len(accumulated_tool_calls))
1368
+ await self.observability_provider.end_span(stream_span)
1369
+ if stream_span.duration_ms():
1370
+ await self.observability_provider.record_metric(
1371
+ "llm.stream.duration", stream_span.duration_ms() or 0, "ms"
1372
+ )
1373
+
1374
+ response = LlmResponse(
1375
+ content=accumulated_content if accumulated_content else None,
1376
+ tool_calls=accumulated_tool_calls if accumulated_tool_calls else None,
1377
+ )
1378
+
1379
+ # Apply after_llm_response middlewares with observability
1380
+ for middleware in self.llm_middlewares:
1381
+ mw_span = None
1382
+ if self.observability_provider:
1383
+ mw_span = await self.observability_provider.create_span(
1384
+ "agent.middleware.after_llm",
1385
+ attributes={
1386
+ "middleware": middleware.__class__.__name__,
1387
+ "stream": True,
1388
+ },
1389
+ )
1390
+
1391
+ response = await middleware.after_llm_response(request, response)
1392
+
1393
+ if self.observability_provider and mw_span:
1394
+ await self.observability_provider.end_span(mw_span)
1395
+ if mw_span.duration_ms():
1396
+ await self.observability_provider.record_metric(
1397
+ "agent.middleware.duration",
1398
+ mw_span.duration_ms() or 0,
1399
+ "ms",
1400
+ tags={
1401
+ "middleware": middleware.__class__.__name__,
1402
+ "phase": "after_llm",
1403
+ "stream": "true",
1404
+ },
1405
+ )
1406
+
1407
+ return response