vanna 0.7.9__py3-none-any.whl → 2.0.0rc1__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.
- vanna/__init__.py +167 -395
- vanna/agents/__init__.py +7 -0
- vanna/capabilities/__init__.py +17 -0
- vanna/capabilities/agent_memory/__init__.py +21 -0
- vanna/capabilities/agent_memory/base.py +103 -0
- vanna/capabilities/agent_memory/models.py +53 -0
- vanna/capabilities/file_system/__init__.py +14 -0
- vanna/capabilities/file_system/base.py +71 -0
- vanna/capabilities/file_system/models.py +25 -0
- vanna/capabilities/sql_runner/__init__.py +13 -0
- vanna/capabilities/sql_runner/base.py +37 -0
- vanna/capabilities/sql_runner/models.py +13 -0
- vanna/components/__init__.py +92 -0
- vanna/components/base.py +11 -0
- vanna/components/rich/__init__.py +83 -0
- vanna/components/rich/containers/__init__.py +7 -0
- vanna/components/rich/containers/card.py +20 -0
- vanna/components/rich/data/__init__.py +9 -0
- vanna/components/rich/data/chart.py +17 -0
- vanna/components/rich/data/dataframe.py +93 -0
- vanna/components/rich/feedback/__init__.py +21 -0
- vanna/components/rich/feedback/badge.py +16 -0
- vanna/components/rich/feedback/icon_text.py +14 -0
- vanna/components/rich/feedback/log_viewer.py +41 -0
- vanna/components/rich/feedback/notification.py +19 -0
- vanna/components/rich/feedback/progress.py +37 -0
- vanna/components/rich/feedback/status_card.py +28 -0
- vanna/components/rich/feedback/status_indicator.py +14 -0
- vanna/components/rich/interactive/__init__.py +21 -0
- vanna/components/rich/interactive/button.py +95 -0
- vanna/components/rich/interactive/task_list.py +58 -0
- vanna/components/rich/interactive/ui_state.py +93 -0
- vanna/components/rich/specialized/__init__.py +7 -0
- vanna/components/rich/specialized/artifact.py +20 -0
- vanna/components/rich/text.py +16 -0
- vanna/components/simple/__init__.py +15 -0
- vanna/components/simple/image.py +15 -0
- vanna/components/simple/link.py +15 -0
- vanna/components/simple/text.py +11 -0
- vanna/core/__init__.py +193 -0
- vanna/core/_compat.py +19 -0
- vanna/core/agent/__init__.py +10 -0
- vanna/core/agent/agent.py +1407 -0
- vanna/core/agent/config.py +123 -0
- vanna/core/audit/__init__.py +28 -0
- vanna/core/audit/base.py +299 -0
- vanna/core/audit/models.py +131 -0
- vanna/core/component_manager.py +329 -0
- vanna/core/components.py +53 -0
- vanna/core/enhancer/__init__.py +11 -0
- vanna/core/enhancer/base.py +94 -0
- vanna/core/enhancer/default.py +118 -0
- vanna/core/enricher/__init__.py +10 -0
- vanna/core/enricher/base.py +59 -0
- vanna/core/errors.py +47 -0
- vanna/core/evaluation/__init__.py +81 -0
- vanna/core/evaluation/base.py +186 -0
- vanna/core/evaluation/dataset.py +254 -0
- vanna/core/evaluation/evaluators.py +376 -0
- vanna/core/evaluation/report.py +289 -0
- vanna/core/evaluation/runner.py +313 -0
- vanna/core/filter/__init__.py +10 -0
- vanna/core/filter/base.py +67 -0
- vanna/core/lifecycle/__init__.py +10 -0
- vanna/core/lifecycle/base.py +83 -0
- vanna/core/llm/__init__.py +16 -0
- vanna/core/llm/base.py +40 -0
- vanna/core/llm/models.py +61 -0
- vanna/core/middleware/__init__.py +10 -0
- vanna/core/middleware/base.py +69 -0
- vanna/core/observability/__init__.py +11 -0
- vanna/core/observability/base.py +88 -0
- vanna/core/observability/models.py +47 -0
- vanna/core/recovery/__init__.py +11 -0
- vanna/core/recovery/base.py +84 -0
- vanna/core/recovery/models.py +32 -0
- vanna/core/registry.py +278 -0
- vanna/core/rich_component.py +156 -0
- vanna/core/simple_component.py +27 -0
- vanna/core/storage/__init__.py +14 -0
- vanna/core/storage/base.py +46 -0
- vanna/core/storage/models.py +46 -0
- vanna/core/system_prompt/__init__.py +13 -0
- vanna/core/system_prompt/base.py +36 -0
- vanna/core/system_prompt/default.py +157 -0
- vanna/core/tool/__init__.py +18 -0
- vanna/core/tool/base.py +70 -0
- vanna/core/tool/models.py +84 -0
- vanna/core/user/__init__.py +17 -0
- vanna/core/user/base.py +29 -0
- vanna/core/user/models.py +25 -0
- vanna/core/user/request_context.py +70 -0
- vanna/core/user/resolver.py +42 -0
- vanna/core/validation.py +164 -0
- vanna/core/workflow/__init__.py +12 -0
- vanna/core/workflow/base.py +254 -0
- vanna/core/workflow/default.py +789 -0
- vanna/examples/__init__.py +1 -0
- vanna/examples/__main__.py +44 -0
- vanna/examples/anthropic_quickstart.py +80 -0
- vanna/examples/artifact_example.py +293 -0
- vanna/examples/claude_sqlite_example.py +236 -0
- vanna/examples/coding_agent_example.py +300 -0
- vanna/examples/custom_system_prompt_example.py +174 -0
- vanna/examples/default_workflow_handler_example.py +208 -0
- vanna/examples/email_auth_example.py +340 -0
- vanna/examples/evaluation_example.py +269 -0
- vanna/examples/extensibility_example.py +262 -0
- vanna/examples/minimal_example.py +67 -0
- vanna/examples/mock_auth_example.py +227 -0
- vanna/examples/mock_custom_tool.py +311 -0
- vanna/examples/mock_quickstart.py +79 -0
- vanna/examples/mock_quota_example.py +145 -0
- vanna/examples/mock_rich_components_demo.py +396 -0
- vanna/examples/mock_sqlite_example.py +223 -0
- vanna/examples/openai_quickstart.py +83 -0
- vanna/examples/primitive_components_demo.py +305 -0
- vanna/examples/quota_lifecycle_example.py +139 -0
- vanna/examples/visualization_example.py +251 -0
- vanna/integrations/__init__.py +17 -0
- vanna/integrations/anthropic/__init__.py +9 -0
- vanna/integrations/anthropic/llm.py +270 -0
- vanna/integrations/azureopenai/__init__.py +9 -0
- vanna/integrations/azureopenai/llm.py +329 -0
- vanna/integrations/azuresearch/__init__.py +7 -0
- vanna/integrations/azuresearch/agent_memory.py +413 -0
- vanna/integrations/bigquery/__init__.py +5 -0
- vanna/integrations/bigquery/sql_runner.py +81 -0
- vanna/integrations/chromadb/__init__.py +104 -0
- vanna/integrations/chromadb/agent_memory.py +416 -0
- vanna/integrations/clickhouse/__init__.py +5 -0
- vanna/integrations/clickhouse/sql_runner.py +82 -0
- vanna/integrations/duckdb/__init__.py +5 -0
- vanna/integrations/duckdb/sql_runner.py +65 -0
- vanna/integrations/faiss/__init__.py +7 -0
- vanna/integrations/faiss/agent_memory.py +431 -0
- vanna/integrations/google/__init__.py +9 -0
- vanna/integrations/google/gemini.py +370 -0
- vanna/integrations/hive/__init__.py +5 -0
- vanna/integrations/hive/sql_runner.py +87 -0
- vanna/integrations/local/__init__.py +17 -0
- vanna/integrations/local/agent_memory/__init__.py +7 -0
- vanna/integrations/local/agent_memory/in_memory.py +285 -0
- vanna/integrations/local/audit.py +59 -0
- vanna/integrations/local/file_system.py +242 -0
- vanna/integrations/local/file_system_conversation_store.py +255 -0
- vanna/integrations/local/storage.py +62 -0
- vanna/integrations/marqo/__init__.py +7 -0
- vanna/integrations/marqo/agent_memory.py +354 -0
- vanna/integrations/milvus/__init__.py +7 -0
- vanna/integrations/milvus/agent_memory.py +458 -0
- vanna/integrations/mock/__init__.py +9 -0
- vanna/integrations/mock/llm.py +65 -0
- vanna/integrations/mssql/__init__.py +5 -0
- vanna/integrations/mssql/sql_runner.py +66 -0
- vanna/integrations/mysql/__init__.py +5 -0
- vanna/integrations/mysql/sql_runner.py +92 -0
- vanna/integrations/ollama/__init__.py +7 -0
- vanna/integrations/ollama/llm.py +252 -0
- vanna/integrations/openai/__init__.py +10 -0
- vanna/integrations/openai/llm.py +267 -0
- vanna/integrations/openai/responses.py +163 -0
- vanna/integrations/opensearch/__init__.py +7 -0
- vanna/integrations/opensearch/agent_memory.py +411 -0
- vanna/integrations/oracle/__init__.py +5 -0
- vanna/integrations/oracle/sql_runner.py +75 -0
- vanna/integrations/pinecone/__init__.py +7 -0
- vanna/integrations/pinecone/agent_memory.py +329 -0
- vanna/integrations/plotly/__init__.py +5 -0
- vanna/integrations/plotly/chart_generator.py +313 -0
- vanna/integrations/postgres/__init__.py +9 -0
- vanna/integrations/postgres/sql_runner.py +112 -0
- vanna/integrations/premium/agent_memory/__init__.py +7 -0
- vanna/integrations/premium/agent_memory/premium.py +186 -0
- vanna/integrations/presto/__init__.py +5 -0
- vanna/integrations/presto/sql_runner.py +107 -0
- vanna/integrations/qdrant/__init__.py +7 -0
- vanna/integrations/qdrant/agent_memory.py +439 -0
- vanna/integrations/snowflake/__init__.py +5 -0
- vanna/integrations/snowflake/sql_runner.py +147 -0
- vanna/integrations/sqlite/__init__.py +9 -0
- vanna/integrations/sqlite/sql_runner.py +65 -0
- vanna/integrations/weaviate/__init__.py +7 -0
- vanna/integrations/weaviate/agent_memory.py +428 -0
- vanna/{ZhipuAI → legacy/ZhipuAI}/ZhipuAI_embeddings.py +11 -11
- vanna/legacy/__init__.py +403 -0
- vanna/legacy/adapter.py +463 -0
- vanna/{advanced → legacy/advanced}/__init__.py +3 -1
- vanna/{anthropic → legacy/anthropic}/anthropic_chat.py +9 -7
- vanna/{azuresearch → legacy/azuresearch}/azuresearch_vector.py +79 -41
- vanna/{base → legacy/base}/base.py +224 -217
- vanna/legacy/bedrock/__init__.py +1 -0
- vanna/{bedrock → legacy/bedrock}/bedrock_converse.py +13 -12
- vanna/{chromadb → legacy/chromadb}/chromadb_vector.py +3 -1
- vanna/legacy/cohere/__init__.py +2 -0
- vanna/{cohere → legacy/cohere}/cohere_chat.py +19 -14
- vanna/{cohere → legacy/cohere}/cohere_embeddings.py +25 -19
- vanna/{deepseek → legacy/deepseek}/deepseek_chat.py +5 -6
- vanna/legacy/faiss/__init__.py +1 -0
- vanna/{faiss → legacy/faiss}/faiss.py +113 -59
- vanna/{flask → legacy/flask}/__init__.py +84 -43
- vanna/{flask → legacy/flask}/assets.py +5 -5
- vanna/{flask → legacy/flask}/auth.py +5 -4
- vanna/{google → legacy/google}/bigquery_vector.py +75 -42
- vanna/{google → legacy/google}/gemini_chat.py +7 -3
- vanna/{hf → legacy/hf}/hf.py +0 -1
- vanna/{milvus → legacy/milvus}/milvus_vector.py +58 -35
- vanna/{mock → legacy/mock}/llm.py +0 -1
- vanna/legacy/mock/vectordb.py +67 -0
- vanna/legacy/ollama/ollama.py +110 -0
- vanna/{openai → legacy/openai}/openai_chat.py +2 -6
- vanna/legacy/opensearch/opensearch_vector.py +369 -0
- vanna/legacy/opensearch/opensearch_vector_semantic.py +200 -0
- vanna/legacy/oracle/oracle_vector.py +584 -0
- vanna/{pgvector → legacy/pgvector}/pgvector.py +42 -13
- vanna/{qdrant → legacy/qdrant}/qdrant.py +2 -6
- vanna/legacy/qianfan/Qianfan_Chat.py +170 -0
- vanna/legacy/qianfan/Qianfan_embeddings.py +36 -0
- vanna/legacy/qianwen/QianwenAI_chat.py +132 -0
- vanna/{remote.py → legacy/remote.py} +28 -26
- vanna/{utils.py → legacy/utils.py} +6 -11
- vanna/{vannadb → legacy/vannadb}/vannadb_vector.py +115 -46
- vanna/{vllm → legacy/vllm}/vllm.py +5 -6
- vanna/{weaviate → legacy/weaviate}/weaviate_vector.py +59 -40
- vanna/{xinference → legacy/xinference}/xinference.py +6 -6
- vanna/py.typed +0 -0
- vanna/servers/__init__.py +16 -0
- vanna/servers/__main__.py +8 -0
- vanna/servers/base/__init__.py +18 -0
- vanna/servers/base/chat_handler.py +65 -0
- vanna/servers/base/models.py +111 -0
- vanna/servers/base/rich_chat_handler.py +141 -0
- vanna/servers/base/templates.py +331 -0
- vanna/servers/cli/__init__.py +7 -0
- vanna/servers/cli/server_runner.py +204 -0
- vanna/servers/fastapi/__init__.py +7 -0
- vanna/servers/fastapi/app.py +163 -0
- vanna/servers/fastapi/routes.py +183 -0
- vanna/servers/flask/__init__.py +7 -0
- vanna/servers/flask/app.py +132 -0
- vanna/servers/flask/routes.py +137 -0
- vanna/tools/__init__.py +41 -0
- vanna/tools/agent_memory.py +322 -0
- vanna/tools/file_system.py +879 -0
- vanna/tools/python.py +222 -0
- vanna/tools/run_sql.py +165 -0
- vanna/tools/visualize_data.py +195 -0
- vanna/utils/__init__.py +0 -0
- vanna/web_components/__init__.py +44 -0
- vanna-2.0.0rc1.dist-info/METADATA +868 -0
- vanna-2.0.0rc1.dist-info/RECORD +289 -0
- vanna-2.0.0rc1.dist-info/entry_points.txt +3 -0
- vanna/bedrock/__init__.py +0 -1
- vanna/cohere/__init__.py +0 -2
- vanna/faiss/__init__.py +0 -1
- vanna/mock/vectordb.py +0 -55
- vanna/ollama/ollama.py +0 -103
- vanna/opensearch/opensearch_vector.py +0 -392
- vanna/opensearch/opensearch_vector_semantic.py +0 -175
- vanna/oracle/oracle_vector.py +0 -585
- vanna/qianfan/Qianfan_Chat.py +0 -165
- vanna/qianfan/Qianfan_embeddings.py +0 -36
- vanna/qianwen/QianwenAI_chat.py +0 -133
- vanna-0.7.9.dist-info/METADATA +0 -408
- vanna-0.7.9.dist-info/RECORD +0 -79
- /vanna/{ZhipuAI → legacy/ZhipuAI}/ZhipuAI_Chat.py +0 -0
- /vanna/{ZhipuAI → legacy/ZhipuAI}/__init__.py +0 -0
- /vanna/{anthropic → legacy/anthropic}/__init__.py +0 -0
- /vanna/{azuresearch → legacy/azuresearch}/__init__.py +0 -0
- /vanna/{base → legacy/base}/__init__.py +0 -0
- /vanna/{chromadb → legacy/chromadb}/__init__.py +0 -0
- /vanna/{deepseek → legacy/deepseek}/__init__.py +0 -0
- /vanna/{exceptions → legacy/exceptions}/__init__.py +0 -0
- /vanna/{google → legacy/google}/__init__.py +0 -0
- /vanna/{hf → legacy/hf}/__init__.py +0 -0
- /vanna/{local.py → legacy/local.py} +0 -0
- /vanna/{marqo → legacy/marqo}/__init__.py +0 -0
- /vanna/{marqo → legacy/marqo}/marqo.py +0 -0
- /vanna/{milvus → legacy/milvus}/__init__.py +0 -0
- /vanna/{mistral → legacy/mistral}/__init__.py +0 -0
- /vanna/{mistral → legacy/mistral}/mistral.py +0 -0
- /vanna/{mock → legacy/mock}/__init__.py +0 -0
- /vanna/{mock → legacy/mock}/embedding.py +0 -0
- /vanna/{ollama → legacy/ollama}/__init__.py +0 -0
- /vanna/{openai → legacy/openai}/__init__.py +0 -0
- /vanna/{openai → legacy/openai}/openai_embeddings.py +0 -0
- /vanna/{opensearch → legacy/opensearch}/__init__.py +0 -0
- /vanna/{oracle → legacy/oracle}/__init__.py +0 -0
- /vanna/{pgvector → legacy/pgvector}/__init__.py +0 -0
- /vanna/{pinecone → legacy/pinecone}/__init__.py +0 -0
- /vanna/{pinecone → legacy/pinecone}/pinecone_vector.py +0 -0
- /vanna/{qdrant → legacy/qdrant}/__init__.py +0 -0
- /vanna/{qianfan → legacy/qianfan}/__init__.py +0 -0
- /vanna/{qianwen → legacy/qianwen}/QianwenAI_embeddings.py +0 -0
- /vanna/{qianwen → legacy/qianwen}/__init__.py +0 -0
- /vanna/{types → legacy/types}/__init__.py +0 -0
- /vanna/{vannadb → legacy/vannadb}/__init__.py +0 -0
- /vanna/{vllm → legacy/vllm}/__init__.py +0 -0
- /vanna/{weaviate → legacy/weaviate}/__init__.py +0 -0
- /vanna/{xinference → legacy/xinference}/__init__.py +0 -0
- {vanna-0.7.9.dist-info → vanna-2.0.0rc1.dist-info}/WHEEL +0 -0
- {vanna-0.7.9.dist-info → vanna-2.0.0rc1.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
|