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.
- 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 +461 -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.0.dist-info/METADATA +485 -0
- vanna-2.0.0.dist-info/RECORD +289 -0
- vanna-2.0.0.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.0.dist-info}/WHEEL +0 -0
- {vanna-0.7.9.dist-info → vanna-2.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base observability provider interface.
|
|
3
|
+
|
|
4
|
+
Observability providers allow you to collect telemetry data about
|
|
5
|
+
agent execution for monitoring and debugging.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from .models import Span, Metric
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ObservabilityProvider(ABC):
|
|
15
|
+
"""Provider for collecting telemetry and observability data.
|
|
16
|
+
|
|
17
|
+
Subclass this to create custom observability integrations that can:
|
|
18
|
+
- Emit metrics to monitoring systems
|
|
19
|
+
- Create distributed traces
|
|
20
|
+
- Log performance data
|
|
21
|
+
- Track costs and usage
|
|
22
|
+
- Monitor error rates
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
class PrometheusProvider(ObservabilityProvider):
|
|
26
|
+
def __init__(self, registry):
|
|
27
|
+
self.registry = registry
|
|
28
|
+
self.request_counter = Counter(
|
|
29
|
+
'agent_requests_total',
|
|
30
|
+
'Total agent requests',
|
|
31
|
+
registry=registry
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
async def record_metric(self, name: str, value: float, tags: Dict[str, str]) -> None:
|
|
35
|
+
if name == "agent.request":
|
|
36
|
+
self.request_counter.inc()
|
|
37
|
+
|
|
38
|
+
async def create_span(self, name: str, attributes: Optional[Dict[str, Any]] = None) -> Span:
|
|
39
|
+
span = Span(name=name, attributes=attributes or {})
|
|
40
|
+
return span
|
|
41
|
+
|
|
42
|
+
agent = AgentRunner(
|
|
43
|
+
llm_service=...,
|
|
44
|
+
observability_provider=PrometheusProvider(registry)
|
|
45
|
+
)
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
async def record_metric(
|
|
49
|
+
self,
|
|
50
|
+
name: str,
|
|
51
|
+
value: float,
|
|
52
|
+
unit: str = "",
|
|
53
|
+
tags: Optional[Dict[str, str]] = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Record a metric measurement.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
name: Metric name (e.g., "agent.request.duration")
|
|
59
|
+
value: Metric value
|
|
60
|
+
unit: Unit of measurement (e.g., "ms", "tokens")
|
|
61
|
+
tags: Additional tags/labels for the metric
|
|
62
|
+
"""
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
async def create_span(
|
|
66
|
+
self, name: str, attributes: Optional[Dict[str, Any]] = None
|
|
67
|
+
) -> Span:
|
|
68
|
+
"""Create a new span for tracing.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
name: Span name/operation
|
|
72
|
+
attributes: Initial span attributes
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Span object to track the operation
|
|
76
|
+
|
|
77
|
+
Note:
|
|
78
|
+
Call span.end() when the operation completes.
|
|
79
|
+
"""
|
|
80
|
+
return Span(name=name, attributes=attributes or {})
|
|
81
|
+
|
|
82
|
+
async def end_span(self, span: Span) -> None:
|
|
83
|
+
"""End a span and record it.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
span: The span to end
|
|
87
|
+
"""
|
|
88
|
+
span.end()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Observability models for spans and metrics.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Span(BaseModel):
|
|
13
|
+
"""Represents a unit of work for distributed tracing."""
|
|
14
|
+
|
|
15
|
+
id: str = Field(default_factory=lambda: str(uuid4()), description="Span ID")
|
|
16
|
+
name: str = Field(description="Span name/operation")
|
|
17
|
+
start_time: float = Field(default_factory=time.time, description="Start timestamp")
|
|
18
|
+
end_time: Optional[float] = Field(default=None, description="End timestamp")
|
|
19
|
+
attributes: Dict[str, Any] = Field(
|
|
20
|
+
default_factory=dict, description="Span attributes"
|
|
21
|
+
)
|
|
22
|
+
parent_id: Optional[str] = Field(default=None, description="Parent span ID")
|
|
23
|
+
|
|
24
|
+
def end(self) -> None:
|
|
25
|
+
"""Mark span as ended."""
|
|
26
|
+
if self.end_time is None:
|
|
27
|
+
self.end_time = time.time()
|
|
28
|
+
|
|
29
|
+
def duration_ms(self) -> Optional[float]:
|
|
30
|
+
"""Get span duration in milliseconds."""
|
|
31
|
+
if self.end_time is None:
|
|
32
|
+
return None
|
|
33
|
+
return (self.end_time - self.start_time) * 1000
|
|
34
|
+
|
|
35
|
+
def set_attribute(self, key: str, value: Any) -> None:
|
|
36
|
+
"""Set a span attribute."""
|
|
37
|
+
self.attributes[key] = value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Metric(BaseModel):
|
|
41
|
+
"""Represents a metric measurement."""
|
|
42
|
+
|
|
43
|
+
name: str = Field(description="Metric name")
|
|
44
|
+
value: float = Field(description="Metric value")
|
|
45
|
+
unit: str = Field(default="", description="Unit of measurement")
|
|
46
|
+
tags: Dict[str, str] = Field(default_factory=dict, description="Metric tags")
|
|
47
|
+
timestamp: float = Field(default_factory=time.time, description="Measurement time")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error recovery system for handling failures gracefully.
|
|
3
|
+
|
|
4
|
+
This module provides interfaces for custom error handling, retry logic,
|
|
5
|
+
and fallback strategies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .base import ErrorRecoveryStrategy
|
|
9
|
+
from .models import RecoveryAction, RecoveryActionType
|
|
10
|
+
|
|
11
|
+
__all__ = ["ErrorRecoveryStrategy", "RecoveryAction", "RecoveryActionType"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base error recovery strategy interface.
|
|
3
|
+
|
|
4
|
+
Recovery strategies allow you to customize how the agent handles errors
|
|
5
|
+
during tool execution and LLM communication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from .models import RecoveryAction, RecoveryActionType
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..tool.models import ToolContext
|
|
15
|
+
from ..llm import LlmRequest
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ErrorRecoveryStrategy(ABC):
|
|
19
|
+
"""Strategy for handling errors and implementing retry logic.
|
|
20
|
+
|
|
21
|
+
Subclass this to create custom error recovery strategies that can:
|
|
22
|
+
- Retry failed operations with backoff
|
|
23
|
+
- Fallback to alternative approaches
|
|
24
|
+
- Log errors to external systems
|
|
25
|
+
- Gracefully degrade functionality
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
class ExponentialBackoffStrategy(ErrorRecoveryStrategy):
|
|
29
|
+
async def handle_tool_error(
|
|
30
|
+
self, error: Exception, context: ToolContext, attempt: int
|
|
31
|
+
) -> RecoveryAction:
|
|
32
|
+
if attempt < 3:
|
|
33
|
+
delay = (2 ** attempt) * 1000 # Exponential backoff
|
|
34
|
+
return RecoveryAction(
|
|
35
|
+
action=RecoveryActionType.RETRY,
|
|
36
|
+
retry_delay_ms=delay,
|
|
37
|
+
message=f"Retrying after {delay}ms"
|
|
38
|
+
)
|
|
39
|
+
return RecoveryAction(
|
|
40
|
+
action=RecoveryActionType.FAIL,
|
|
41
|
+
message="Max retries exceeded"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
agent = AgentRunner(
|
|
45
|
+
llm_service=...,
|
|
46
|
+
error_recovery_strategy=ExponentialBackoffStrategy()
|
|
47
|
+
)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
async def handle_tool_error(
|
|
51
|
+
self, error: Exception, context: "ToolContext", attempt: int = 1
|
|
52
|
+
) -> RecoveryAction:
|
|
53
|
+
"""Handle errors during tool execution.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
error: The exception that occurred
|
|
57
|
+
context: Tool execution context
|
|
58
|
+
attempt: Current attempt number (1-indexed)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
RecoveryAction indicating how to proceed
|
|
62
|
+
"""
|
|
63
|
+
# Default: fail immediately
|
|
64
|
+
return RecoveryAction(
|
|
65
|
+
action=RecoveryActionType.FAIL, message=f"Tool error: {str(error)}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
async def handle_llm_error(
|
|
69
|
+
self, error: Exception, request: "LlmRequest", attempt: int = 1
|
|
70
|
+
) -> RecoveryAction:
|
|
71
|
+
"""Handle errors during LLM communication.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
error: The exception that occurred
|
|
75
|
+
request: The LLM request that failed
|
|
76
|
+
attempt: Current attempt number (1-indexed)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
RecoveryAction indicating how to proceed
|
|
80
|
+
"""
|
|
81
|
+
# Default: fail immediately
|
|
82
|
+
return RecoveryAction(
|
|
83
|
+
action=RecoveryActionType.FAIL, message=f"LLM error: {str(error)}"
|
|
84
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recovery action models for error handling.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RecoveryActionType(str, Enum):
|
|
12
|
+
"""Types of recovery actions."""
|
|
13
|
+
|
|
14
|
+
RETRY = "retry"
|
|
15
|
+
FAIL = "fail"
|
|
16
|
+
FALLBACK = "fallback"
|
|
17
|
+
SKIP = "skip"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RecoveryAction(BaseModel):
|
|
21
|
+
"""Action to take when recovering from an error."""
|
|
22
|
+
|
|
23
|
+
action: RecoveryActionType = Field(description="Type of recovery action")
|
|
24
|
+
retry_delay_ms: Optional[int] = Field(
|
|
25
|
+
default=None, description="Delay before retry in milliseconds"
|
|
26
|
+
)
|
|
27
|
+
fallback_value: Optional[Any] = Field(
|
|
28
|
+
default=None, description="Fallback value to use"
|
|
29
|
+
)
|
|
30
|
+
message: Optional[str] = Field(
|
|
31
|
+
default=None, description="Message to include with action"
|
|
32
|
+
)
|
vanna/core/registry.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool registry for the Vanna Agents framework.
|
|
3
|
+
|
|
4
|
+
This module provides the ToolRegistry class for managing and executing tools.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, TypeVar, Union
|
|
9
|
+
|
|
10
|
+
from .tool import Tool, ToolCall, ToolContext, ToolRejection, ToolResult, ToolSchema
|
|
11
|
+
from .user import User
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .audit import AuditLogger
|
|
15
|
+
from .agent.config import AuditConfig
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _LocalToolWrapper(Tool[T]):
|
|
21
|
+
"""Wrapper for tools with configurable access groups."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, wrapped_tool: Tool[T], access_groups: List[str]):
|
|
24
|
+
self._wrapped_tool = wrapped_tool
|
|
25
|
+
self._access_groups = access_groups
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def name(self) -> str:
|
|
29
|
+
return self._wrapped_tool.name
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def description(self) -> str:
|
|
33
|
+
return self._wrapped_tool.description
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def access_groups(self) -> List[str]:
|
|
37
|
+
return self._access_groups
|
|
38
|
+
|
|
39
|
+
def get_args_schema(self) -> Type[T]:
|
|
40
|
+
return self._wrapped_tool.get_args_schema()
|
|
41
|
+
|
|
42
|
+
async def execute(self, context: ToolContext, args: T) -> ToolResult:
|
|
43
|
+
return await self._wrapped_tool.execute(context, args)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ToolRegistry:
|
|
47
|
+
"""Registry for managing tools."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
audit_logger: Optional["AuditLogger"] = None,
|
|
52
|
+
audit_config: Optional["AuditConfig"] = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
self._tools: Dict[str, Tool[Any]] = {}
|
|
55
|
+
self.audit_logger = audit_logger
|
|
56
|
+
if audit_config is not None:
|
|
57
|
+
self.audit_config = audit_config
|
|
58
|
+
else:
|
|
59
|
+
from .agent.config import AuditConfig
|
|
60
|
+
|
|
61
|
+
self.audit_config = AuditConfig()
|
|
62
|
+
|
|
63
|
+
def register_local_tool(self, tool: Tool[Any], access_groups: List[str]) -> None:
|
|
64
|
+
"""Register a local tool with optional access group restrictions.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
tool: The tool to register
|
|
68
|
+
access_groups: List of groups that can access this tool.
|
|
69
|
+
If None or empty, tool is accessible to all users.
|
|
70
|
+
"""
|
|
71
|
+
if tool.name in self._tools:
|
|
72
|
+
raise ValueError(f"Tool '{tool.name}' already registered")
|
|
73
|
+
|
|
74
|
+
if access_groups:
|
|
75
|
+
# Wrap the tool with access groups
|
|
76
|
+
wrapped_tool = _LocalToolWrapper(tool, access_groups)
|
|
77
|
+
self._tools[tool.name] = wrapped_tool
|
|
78
|
+
else:
|
|
79
|
+
# No access restrictions, register as-is
|
|
80
|
+
self._tools[tool.name] = tool
|
|
81
|
+
|
|
82
|
+
async def get_tool(self, name: str) -> Optional[Tool[Any]]:
|
|
83
|
+
"""Get a tool by name."""
|
|
84
|
+
return self._tools.get(name)
|
|
85
|
+
|
|
86
|
+
async def list_tools(self) -> List[str]:
|
|
87
|
+
"""List all registered tool names."""
|
|
88
|
+
return list(self._tools.keys())
|
|
89
|
+
|
|
90
|
+
async def get_schemas(self, user: Optional[User] = None) -> List[ToolSchema]:
|
|
91
|
+
"""Get schemas for all tools accessible to user."""
|
|
92
|
+
schemas = []
|
|
93
|
+
for tool in self._tools.values():
|
|
94
|
+
if user is None or await self._validate_tool_permissions(tool, user):
|
|
95
|
+
schemas.append(tool.get_schema())
|
|
96
|
+
return schemas
|
|
97
|
+
|
|
98
|
+
async def _validate_tool_permissions(self, tool: Tool[Any], user: User) -> bool:
|
|
99
|
+
"""Validate if user has access to tool based on group membership.
|
|
100
|
+
|
|
101
|
+
Checks for intersection between user's group memberships and tool's access groups.
|
|
102
|
+
If tool has no access groups specified, it's accessible to all users.
|
|
103
|
+
"""
|
|
104
|
+
tool_access_groups = tool.access_groups
|
|
105
|
+
if not tool_access_groups:
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
user_groups = set(user.group_memberships)
|
|
109
|
+
tool_groups = set(tool_access_groups)
|
|
110
|
+
# Grant access if any group in user.group_memberships exists in tool.access_groups
|
|
111
|
+
return bool(user_groups & tool_groups)
|
|
112
|
+
|
|
113
|
+
async def transform_args(
|
|
114
|
+
self,
|
|
115
|
+
tool: Tool[T],
|
|
116
|
+
args: T,
|
|
117
|
+
user: User,
|
|
118
|
+
context: ToolContext,
|
|
119
|
+
) -> Union[T, ToolRejection]:
|
|
120
|
+
"""Transform and validate tool arguments based on user context.
|
|
121
|
+
|
|
122
|
+
This method allows per-user transformation of tool arguments, such as:
|
|
123
|
+
- Applying row-level security (RLS) to SQL queries
|
|
124
|
+
- Filtering available options based on user permissions
|
|
125
|
+
- Validating required arguments are present
|
|
126
|
+
- Redacting sensitive fields
|
|
127
|
+
|
|
128
|
+
The default implementation performs no transformation (NoOp).
|
|
129
|
+
Subclasses can override this method to implement custom transformation logic.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
tool: The tool being executed
|
|
133
|
+
args: Already Pydantic-validated arguments
|
|
134
|
+
user: The user executing the tool
|
|
135
|
+
context: Full execution context
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Either:
|
|
139
|
+
- Transformed arguments (may be unchanged if no transformation needed)
|
|
140
|
+
- ToolRejection with explanation of why args were rejected
|
|
141
|
+
"""
|
|
142
|
+
return args # Default: no transformation (NoOp)
|
|
143
|
+
|
|
144
|
+
async def execute(
|
|
145
|
+
self,
|
|
146
|
+
tool_call: ToolCall,
|
|
147
|
+
context: ToolContext,
|
|
148
|
+
) -> ToolResult:
|
|
149
|
+
"""Execute a tool call with validation."""
|
|
150
|
+
tool = await self.get_tool(tool_call.name)
|
|
151
|
+
if not tool:
|
|
152
|
+
msg = f"Tool '{tool_call.name}' not found"
|
|
153
|
+
return ToolResult(
|
|
154
|
+
success=False,
|
|
155
|
+
result_for_llm=msg,
|
|
156
|
+
ui_component=None,
|
|
157
|
+
error=msg,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Validate group access
|
|
161
|
+
if not await self._validate_tool_permissions(tool, context.user):
|
|
162
|
+
msg = f"Insufficient group access for tool '{tool_call.name}'"
|
|
163
|
+
|
|
164
|
+
# Audit access denial
|
|
165
|
+
if (
|
|
166
|
+
self.audit_logger
|
|
167
|
+
and self.audit_config
|
|
168
|
+
and self.audit_config.log_tool_access_checks
|
|
169
|
+
):
|
|
170
|
+
await self.audit_logger.log_tool_access_check(
|
|
171
|
+
user=context.user,
|
|
172
|
+
tool_name=tool_call.name,
|
|
173
|
+
access_granted=False,
|
|
174
|
+
required_groups=tool.access_groups,
|
|
175
|
+
context=context,
|
|
176
|
+
reason=msg,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return ToolResult(
|
|
180
|
+
success=False,
|
|
181
|
+
result_for_llm=msg,
|
|
182
|
+
ui_component=None,
|
|
183
|
+
error=msg,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Validate and parse arguments
|
|
187
|
+
try:
|
|
188
|
+
args_model = tool.get_args_schema()
|
|
189
|
+
validated_args = args_model.model_validate(tool_call.arguments)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
msg = f"Invalid arguments: {str(e)}"
|
|
192
|
+
return ToolResult(
|
|
193
|
+
success=False,
|
|
194
|
+
result_for_llm=msg,
|
|
195
|
+
ui_component=None,
|
|
196
|
+
error=msg,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Transform/validate arguments based on user context
|
|
200
|
+
transform_result = await self.transform_args(
|
|
201
|
+
tool=tool,
|
|
202
|
+
args=validated_args,
|
|
203
|
+
user=context.user,
|
|
204
|
+
context=context,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if isinstance(transform_result, ToolRejection):
|
|
208
|
+
return ToolResult(
|
|
209
|
+
success=False,
|
|
210
|
+
result_for_llm=transform_result.reason,
|
|
211
|
+
ui_component=None,
|
|
212
|
+
error=transform_result.reason,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Use transformed arguments for execution
|
|
216
|
+
final_args = transform_result
|
|
217
|
+
|
|
218
|
+
# Audit successful access check
|
|
219
|
+
if (
|
|
220
|
+
self.audit_logger
|
|
221
|
+
and self.audit_config
|
|
222
|
+
and self.audit_config.log_tool_access_checks
|
|
223
|
+
):
|
|
224
|
+
await self.audit_logger.log_tool_access_check(
|
|
225
|
+
user=context.user,
|
|
226
|
+
tool_name=tool_call.name,
|
|
227
|
+
access_granted=True,
|
|
228
|
+
required_groups=tool.access_groups,
|
|
229
|
+
context=context,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Audit tool invocation
|
|
233
|
+
if (
|
|
234
|
+
self.audit_logger
|
|
235
|
+
and self.audit_config
|
|
236
|
+
and self.audit_config.log_tool_invocations
|
|
237
|
+
):
|
|
238
|
+
# Get UI features if available from context
|
|
239
|
+
ui_features = context.metadata.get("ui_features_available", [])
|
|
240
|
+
await self.audit_logger.log_tool_invocation(
|
|
241
|
+
user=context.user,
|
|
242
|
+
tool_call=tool_call,
|
|
243
|
+
ui_features=ui_features,
|
|
244
|
+
context=context,
|
|
245
|
+
sanitize_parameters=self.audit_config.sanitize_tool_parameters,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Execute tool with context-first signature
|
|
249
|
+
try:
|
|
250
|
+
start_time = time.perf_counter()
|
|
251
|
+
result = await tool.execute(context, final_args)
|
|
252
|
+
execution_time_ms = (time.perf_counter() - start_time) * 1000
|
|
253
|
+
|
|
254
|
+
# Add execution time to metadata
|
|
255
|
+
result.metadata["execution_time_ms"] = execution_time_ms
|
|
256
|
+
|
|
257
|
+
# Audit tool result
|
|
258
|
+
if (
|
|
259
|
+
self.audit_logger
|
|
260
|
+
and self.audit_config
|
|
261
|
+
and self.audit_config.log_tool_results
|
|
262
|
+
):
|
|
263
|
+
await self.audit_logger.log_tool_result(
|
|
264
|
+
user=context.user,
|
|
265
|
+
tool_call=tool_call,
|
|
266
|
+
result=result,
|
|
267
|
+
context=context,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return result
|
|
271
|
+
except Exception as e:
|
|
272
|
+
msg = f"Execution failed: {str(e)}"
|
|
273
|
+
return ToolResult(
|
|
274
|
+
success=False,
|
|
275
|
+
result_for_llm=msg,
|
|
276
|
+
ui_component=None,
|
|
277
|
+
error=msg,
|
|
278
|
+
)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base classes for rich UI components.
|
|
3
|
+
|
|
4
|
+
This module provides the base RichComponent class and supporting enums
|
|
5
|
+
for the component system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any, Dict, List, TypeVar
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
# Type variable for self-returning methods
|
|
16
|
+
T = TypeVar("T", bound="RichComponent")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ComponentType(str, Enum):
|
|
20
|
+
"""Types of rich UI components."""
|
|
21
|
+
|
|
22
|
+
# Basic components
|
|
23
|
+
TEXT = "text"
|
|
24
|
+
CARD = "card"
|
|
25
|
+
CONTAINER = "container"
|
|
26
|
+
|
|
27
|
+
# Primitive UI components (domain-agnostic)
|
|
28
|
+
STATUS_CARD = "status_card"
|
|
29
|
+
PROGRESS_DISPLAY = "progress_display"
|
|
30
|
+
LOG_VIEWER = "log_viewer"
|
|
31
|
+
BADGE = "badge"
|
|
32
|
+
ICON_TEXT = "icon_text"
|
|
33
|
+
|
|
34
|
+
# Interactive components
|
|
35
|
+
TASK_LIST = "task_list"
|
|
36
|
+
PROGRESS_BAR = "progress_bar"
|
|
37
|
+
BUTTON = "button"
|
|
38
|
+
BUTTON_GROUP = "button_group"
|
|
39
|
+
|
|
40
|
+
# Data components
|
|
41
|
+
TABLE = "table"
|
|
42
|
+
DATAFRAME = "dataframe"
|
|
43
|
+
CHART = "chart"
|
|
44
|
+
CODE_BLOCK = "code_block"
|
|
45
|
+
|
|
46
|
+
# Status components
|
|
47
|
+
STATUS_INDICATOR = "status_indicator"
|
|
48
|
+
NOTIFICATION = "notification"
|
|
49
|
+
ALERT = "alert"
|
|
50
|
+
|
|
51
|
+
# Artifact components
|
|
52
|
+
ARTIFACT = "artifact"
|
|
53
|
+
|
|
54
|
+
# UI state components
|
|
55
|
+
STATUS_BAR_UPDATE = "status_bar_update"
|
|
56
|
+
TASK_TRACKER_UPDATE = "task_tracker_update"
|
|
57
|
+
CHAT_INPUT_UPDATE = "chat_input_update"
|
|
58
|
+
|
|
59
|
+
# Legacy (deprecated - use primitives instead)
|
|
60
|
+
TOOL_EXECUTION = "tool_execution"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ComponentLifecycle(str, Enum):
|
|
64
|
+
"""Component lifecycle operations."""
|
|
65
|
+
|
|
66
|
+
CREATE = "create"
|
|
67
|
+
UPDATE = "update"
|
|
68
|
+
REPLACE = "replace"
|
|
69
|
+
REMOVE = "remove"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class RichComponent(BaseModel):
|
|
73
|
+
"""Base class for all rich UI components."""
|
|
74
|
+
|
|
75
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
76
|
+
type: ComponentType
|
|
77
|
+
lifecycle: ComponentLifecycle = ComponentLifecycle.CREATE
|
|
78
|
+
data: Dict[str, Any] = Field(default_factory=dict)
|
|
79
|
+
children: List[str] = Field(default_factory=list) # Child component IDs
|
|
80
|
+
timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
|
|
81
|
+
visible: bool = True
|
|
82
|
+
interactive: bool = False
|
|
83
|
+
|
|
84
|
+
def update(self: T, **kwargs: Any) -> T:
|
|
85
|
+
"""Create an updated copy of this component."""
|
|
86
|
+
updated_data = self.model_dump()
|
|
87
|
+
updated_data.update(kwargs)
|
|
88
|
+
updated_data["lifecycle"] = ComponentLifecycle.UPDATE
|
|
89
|
+
updated_data["timestamp"] = datetime.utcnow().isoformat()
|
|
90
|
+
return self.__class__(**updated_data)
|
|
91
|
+
|
|
92
|
+
def hide(self: T) -> T:
|
|
93
|
+
"""Create a hidden copy of this component."""
|
|
94
|
+
return self.update(visible=False)
|
|
95
|
+
|
|
96
|
+
def show(self: T) -> T:
|
|
97
|
+
"""Create a visible copy of this component."""
|
|
98
|
+
return self.update(visible=True)
|
|
99
|
+
|
|
100
|
+
def serialize_for_frontend(self) -> Dict[str, Any]:
|
|
101
|
+
"""Normalize component payload for the frontend renderer.
|
|
102
|
+
|
|
103
|
+
The frontend expects component-specific fields to live under the
|
|
104
|
+
``data`` key while the shared metadata (``id``, ``type``, layout hints,
|
|
105
|
+
etc.) remains at the top level. Pydantic's ``model_dump`` keeps
|
|
106
|
+
component attributes at the top level, so we remap them here before
|
|
107
|
+
streaming them across the wire.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
# Base fields that should remain at the top level of the payload.
|
|
111
|
+
shared_fields = {
|
|
112
|
+
"id",
|
|
113
|
+
"type",
|
|
114
|
+
"lifecycle",
|
|
115
|
+
"children",
|
|
116
|
+
"timestamp",
|
|
117
|
+
"visible",
|
|
118
|
+
"interactive",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
raw = self.model_dump()
|
|
122
|
+
payload: Dict[str, Any] = {}
|
|
123
|
+
|
|
124
|
+
# Preserve any existing data payload so implementations can opt-in to
|
|
125
|
+
# advanced usage without losing information.
|
|
126
|
+
raw_data = raw.get("data")
|
|
127
|
+
if raw_data is not None and isinstance(raw_data, dict):
|
|
128
|
+
component_data: Dict[str, Any] = raw_data.copy()
|
|
129
|
+
else:
|
|
130
|
+
# Handle case where data might be a sequence or other type, or None
|
|
131
|
+
component_data = {}
|
|
132
|
+
|
|
133
|
+
for key, value in raw.items():
|
|
134
|
+
if key in shared_fields:
|
|
135
|
+
payload[key] = value
|
|
136
|
+
elif key == "data":
|
|
137
|
+
# For most components, skip the base data field
|
|
138
|
+
continue
|
|
139
|
+
elif (
|
|
140
|
+
key == "rows"
|
|
141
|
+
and hasattr(self, "type")
|
|
142
|
+
and self.type.value == "dataframe"
|
|
143
|
+
):
|
|
144
|
+
# For DataFrame components, the 'rows' field contains the actual row data
|
|
145
|
+
# which should be included in the component_data as 'data' for the frontend
|
|
146
|
+
component_data["data"] = value
|
|
147
|
+
else:
|
|
148
|
+
component_data[key] = value
|
|
149
|
+
|
|
150
|
+
payload["data"] = component_data
|
|
151
|
+
|
|
152
|
+
# Ensure enums are serialized as primitive values for the frontend.
|
|
153
|
+
payload["type"] = self.type.value
|
|
154
|
+
payload["lifecycle"] = self.lifecycle.value
|
|
155
|
+
|
|
156
|
+
return payload
|