vanna 0.7.8__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 +247 -223
- 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.8.dist-info/METADATA +0 -408
- vanna-0.7.8.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.8.dist-info → vanna-2.0.0.dist-info}/WHEEL +0 -0
- {vanna-0.7.8.dist-info → vanna-2.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pinecone vector database implementation of AgentMemory.
|
|
3
|
+
|
|
4
|
+
This implementation uses Pinecone for cloud-based vector storage of tool usage patterns.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
import asyncio
|
|
12
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from pinecone import Pinecone, ServerlessSpec
|
|
16
|
+
|
|
17
|
+
PINECONE_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
PINECONE_AVAILABLE = False
|
|
20
|
+
|
|
21
|
+
from vanna.capabilities.agent_memory import (
|
|
22
|
+
AgentMemory,
|
|
23
|
+
TextMemory,
|
|
24
|
+
TextMemorySearchResult,
|
|
25
|
+
ToolMemory,
|
|
26
|
+
ToolMemorySearchResult,
|
|
27
|
+
)
|
|
28
|
+
from vanna.core.tool import ToolContext
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PineconeAgentMemory(AgentMemory):
|
|
32
|
+
"""Pinecone-based implementation of AgentMemory."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
api_key: str,
|
|
37
|
+
index_name: str = "tool-memories",
|
|
38
|
+
environment: str = "us-east-1",
|
|
39
|
+
dimension: int = 384,
|
|
40
|
+
metric: str = "cosine",
|
|
41
|
+
):
|
|
42
|
+
if not PINECONE_AVAILABLE:
|
|
43
|
+
raise ImportError(
|
|
44
|
+
"Pinecone is required for PineconeAgentMemory. Install with: pip install pinecone-client"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
self.api_key = api_key
|
|
48
|
+
self.index_name = index_name
|
|
49
|
+
self.environment = environment
|
|
50
|
+
self.dimension = dimension
|
|
51
|
+
self.metric = metric
|
|
52
|
+
self._client = None
|
|
53
|
+
self._index = None
|
|
54
|
+
self._executor = ThreadPoolExecutor(max_workers=2)
|
|
55
|
+
|
|
56
|
+
def _get_client(self):
|
|
57
|
+
"""Get or create Pinecone client."""
|
|
58
|
+
if self._client is None:
|
|
59
|
+
self._client = Pinecone(api_key=self.api_key)
|
|
60
|
+
return self._client
|
|
61
|
+
|
|
62
|
+
def _get_index(self):
|
|
63
|
+
"""Get or create Pinecone index."""
|
|
64
|
+
if self._index is None:
|
|
65
|
+
client = self._get_client()
|
|
66
|
+
|
|
67
|
+
# Create index if it doesn't exist
|
|
68
|
+
if self.index_name not in client.list_indexes().names():
|
|
69
|
+
client.create_index(
|
|
70
|
+
name=self.index_name,
|
|
71
|
+
dimension=self.dimension,
|
|
72
|
+
metric=self.metric,
|
|
73
|
+
spec=ServerlessSpec(cloud="aws", region=self.environment),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
self._index = client.Index(self.index_name)
|
|
77
|
+
return self._index
|
|
78
|
+
|
|
79
|
+
def _create_embedding(self, text: str) -> List[float]:
|
|
80
|
+
"""Create a simple embedding from text (placeholder - should use actual embedding model)."""
|
|
81
|
+
# TODO: Replace with actual embedding model
|
|
82
|
+
import hashlib
|
|
83
|
+
|
|
84
|
+
hash_val = int(hashlib.md5(text.encode()).hexdigest(), 16)
|
|
85
|
+
return [(hash_val >> i) % 100 / 100.0 for i in range(self.dimension)]
|
|
86
|
+
|
|
87
|
+
async def save_tool_usage(
|
|
88
|
+
self,
|
|
89
|
+
question: str,
|
|
90
|
+
tool_name: str,
|
|
91
|
+
args: Dict[str, Any],
|
|
92
|
+
context: ToolContext,
|
|
93
|
+
success: bool = True,
|
|
94
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Save a tool usage pattern."""
|
|
97
|
+
|
|
98
|
+
def _save():
|
|
99
|
+
index = self._get_index()
|
|
100
|
+
|
|
101
|
+
memory_id = str(uuid.uuid4())
|
|
102
|
+
timestamp = datetime.now().isoformat()
|
|
103
|
+
embedding = self._create_embedding(question)
|
|
104
|
+
|
|
105
|
+
# Pinecone metadata must be simple types
|
|
106
|
+
memory_metadata = {
|
|
107
|
+
"question": question,
|
|
108
|
+
"tool_name": tool_name,
|
|
109
|
+
"args_json": json.dumps(args),
|
|
110
|
+
"timestamp": timestamp,
|
|
111
|
+
"success": success,
|
|
112
|
+
"metadata_json": json.dumps(metadata or {}),
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
index.upsert(vectors=[(memory_id, embedding, memory_metadata)])
|
|
116
|
+
|
|
117
|
+
await asyncio.get_event_loop().run_in_executor(self._executor, _save)
|
|
118
|
+
|
|
119
|
+
async def search_similar_usage(
|
|
120
|
+
self,
|
|
121
|
+
question: str,
|
|
122
|
+
context: ToolContext,
|
|
123
|
+
*,
|
|
124
|
+
limit: int = 10,
|
|
125
|
+
similarity_threshold: float = 0.7,
|
|
126
|
+
tool_name_filter: Optional[str] = None,
|
|
127
|
+
) -> List[ToolMemorySearchResult]:
|
|
128
|
+
"""Search for similar tool usage patterns."""
|
|
129
|
+
|
|
130
|
+
def _search():
|
|
131
|
+
index = self._get_index()
|
|
132
|
+
|
|
133
|
+
embedding = self._create_embedding(question)
|
|
134
|
+
|
|
135
|
+
# Build filter
|
|
136
|
+
filter_dict = {"success": True}
|
|
137
|
+
if tool_name_filter:
|
|
138
|
+
filter_dict["tool_name"] = tool_name_filter
|
|
139
|
+
|
|
140
|
+
results = index.query(
|
|
141
|
+
vector=embedding, top_k=limit, filter=filter_dict, include_metadata=True
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
search_results = []
|
|
145
|
+
for i, match in enumerate(results.matches):
|
|
146
|
+
# Pinecone returns similarity score directly
|
|
147
|
+
similarity_score = match.score
|
|
148
|
+
|
|
149
|
+
if similarity_score >= similarity_threshold:
|
|
150
|
+
metadata = match.metadata
|
|
151
|
+
args = json.loads(metadata.get("args_json", "{}"))
|
|
152
|
+
metadata_dict = json.loads(metadata.get("metadata_json", "{}"))
|
|
153
|
+
|
|
154
|
+
memory = ToolMemory(
|
|
155
|
+
memory_id=match.id,
|
|
156
|
+
question=metadata["question"],
|
|
157
|
+
tool_name=metadata["tool_name"],
|
|
158
|
+
args=args,
|
|
159
|
+
timestamp=metadata.get("timestamp"),
|
|
160
|
+
success=metadata.get("success", True),
|
|
161
|
+
metadata=metadata_dict,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
search_results.append(
|
|
165
|
+
ToolMemorySearchResult(
|
|
166
|
+
memory=memory, similarity_score=similarity_score, rank=i + 1
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return search_results
|
|
171
|
+
|
|
172
|
+
return await asyncio.get_event_loop().run_in_executor(self._executor, _search)
|
|
173
|
+
|
|
174
|
+
async def get_recent_memories(
|
|
175
|
+
self, context: ToolContext, limit: int = 10
|
|
176
|
+
) -> List[ToolMemory]:
|
|
177
|
+
"""Get recently added memories."""
|
|
178
|
+
|
|
179
|
+
def _get_recent():
|
|
180
|
+
index = self._get_index()
|
|
181
|
+
|
|
182
|
+
# Pinecone doesn't have a native "get all" - we need to query with a dummy vector
|
|
183
|
+
# or use the list operation with metadata filtering
|
|
184
|
+
# This is a limitation - we'll return empty for now
|
|
185
|
+
# In production, you'd maintain a separate timestamp index or use Pinecone's metadata filtering
|
|
186
|
+
return []
|
|
187
|
+
|
|
188
|
+
return await asyncio.get_event_loop().run_in_executor(
|
|
189
|
+
self._executor, _get_recent
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:
|
|
193
|
+
"""Delete a memory by its ID."""
|
|
194
|
+
|
|
195
|
+
def _delete():
|
|
196
|
+
index = self._get_index()
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
index.delete(ids=[memory_id])
|
|
200
|
+
return True
|
|
201
|
+
except Exception:
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)
|
|
205
|
+
|
|
206
|
+
async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:
|
|
207
|
+
"""Save a text memory."""
|
|
208
|
+
|
|
209
|
+
def _save():
|
|
210
|
+
index = self._get_index()
|
|
211
|
+
|
|
212
|
+
memory_id = str(uuid.uuid4())
|
|
213
|
+
timestamp = datetime.now().isoformat()
|
|
214
|
+
embedding = self._create_embedding(content)
|
|
215
|
+
|
|
216
|
+
memory_metadata = {
|
|
217
|
+
"content": content,
|
|
218
|
+
"timestamp": timestamp,
|
|
219
|
+
"is_text_memory": True,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
index.upsert(vectors=[(memory_id, embedding, memory_metadata)])
|
|
223
|
+
|
|
224
|
+
return TextMemory(memory_id=memory_id, content=content, timestamp=timestamp)
|
|
225
|
+
|
|
226
|
+
return await asyncio.get_event_loop().run_in_executor(self._executor, _save)
|
|
227
|
+
|
|
228
|
+
async def search_text_memories(
|
|
229
|
+
self,
|
|
230
|
+
query: str,
|
|
231
|
+
context: ToolContext,
|
|
232
|
+
*,
|
|
233
|
+
limit: int = 10,
|
|
234
|
+
similarity_threshold: float = 0.7,
|
|
235
|
+
) -> List[TextMemorySearchResult]:
|
|
236
|
+
"""Search for similar text memories."""
|
|
237
|
+
|
|
238
|
+
def _search():
|
|
239
|
+
index = self._get_index()
|
|
240
|
+
|
|
241
|
+
embedding = self._create_embedding(query)
|
|
242
|
+
|
|
243
|
+
filter_dict = {"is_text_memory": True}
|
|
244
|
+
|
|
245
|
+
results = index.query(
|
|
246
|
+
vector=embedding, top_k=limit, filter=filter_dict, include_metadata=True
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
search_results = []
|
|
250
|
+
for i, match in enumerate(results.matches):
|
|
251
|
+
similarity_score = match.score
|
|
252
|
+
|
|
253
|
+
if similarity_score >= similarity_threshold:
|
|
254
|
+
metadata = match.metadata
|
|
255
|
+
|
|
256
|
+
memory = TextMemory(
|
|
257
|
+
memory_id=match.id,
|
|
258
|
+
content=metadata.get("content", ""),
|
|
259
|
+
timestamp=metadata.get("timestamp"),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
search_results.append(
|
|
263
|
+
TextMemorySearchResult(
|
|
264
|
+
memory=memory, similarity_score=similarity_score, rank=i + 1
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return search_results
|
|
269
|
+
|
|
270
|
+
return await asyncio.get_event_loop().run_in_executor(self._executor, _search)
|
|
271
|
+
|
|
272
|
+
async def get_recent_text_memories(
|
|
273
|
+
self, context: ToolContext, limit: int = 10
|
|
274
|
+
) -> List[TextMemory]:
|
|
275
|
+
"""Get recently added text memories."""
|
|
276
|
+
|
|
277
|
+
def _get_recent():
|
|
278
|
+
# Pinecone doesn't have a native "get all sorted by timestamp" operation
|
|
279
|
+
# This is a limitation - returning empty list
|
|
280
|
+
# In production, you'd need to maintain a separate index or use metadata filtering
|
|
281
|
+
return []
|
|
282
|
+
|
|
283
|
+
return await asyncio.get_event_loop().run_in_executor(
|
|
284
|
+
self._executor, _get_recent
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:
|
|
288
|
+
"""Delete a text memory by its ID."""
|
|
289
|
+
|
|
290
|
+
def _delete():
|
|
291
|
+
index = self._get_index()
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
index.delete(ids=[memory_id])
|
|
295
|
+
return True
|
|
296
|
+
except Exception:
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)
|
|
300
|
+
|
|
301
|
+
async def clear_memories(
|
|
302
|
+
self,
|
|
303
|
+
context: ToolContext,
|
|
304
|
+
tool_name: Optional[str] = None,
|
|
305
|
+
before_date: Optional[str] = None,
|
|
306
|
+
) -> int:
|
|
307
|
+
"""Clear stored memories."""
|
|
308
|
+
|
|
309
|
+
def _clear():
|
|
310
|
+
index = self._get_index()
|
|
311
|
+
|
|
312
|
+
# Build filter
|
|
313
|
+
filter_dict = {}
|
|
314
|
+
if tool_name:
|
|
315
|
+
filter_dict["tool_name"] = tool_name
|
|
316
|
+
if before_date:
|
|
317
|
+
filter_dict["timestamp"] = {"$lt": before_date}
|
|
318
|
+
|
|
319
|
+
if filter_dict:
|
|
320
|
+
# Delete with filter
|
|
321
|
+
index.delete(filter=filter_dict)
|
|
322
|
+
else:
|
|
323
|
+
# Delete all
|
|
324
|
+
index.delete(delete_all=True)
|
|
325
|
+
|
|
326
|
+
# Pinecone doesn't return count of deleted items
|
|
327
|
+
return 0
|
|
328
|
+
|
|
329
|
+
return await asyncio.get_event_loop().run_in_executor(self._executor, _clear)
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Plotly-based chart generator with automatic chart type selection."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, List, cast
|
|
4
|
+
import json
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import plotly.graph_objects as go
|
|
7
|
+
import plotly.express as px
|
|
8
|
+
import plotly.io as pio
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlotlyChartGenerator:
|
|
12
|
+
"""Generate Plotly charts using heuristics based on DataFrame characteristics."""
|
|
13
|
+
|
|
14
|
+
# Vanna brand colors from landing page
|
|
15
|
+
THEME_COLORS = {
|
|
16
|
+
"navy": "#023d60",
|
|
17
|
+
"cream": "#e7e1cf",
|
|
18
|
+
"teal": "#15a8a8",
|
|
19
|
+
"orange": "#fe5d26",
|
|
20
|
+
"magenta": "#bf1363",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Color palette for charts (excluding cream as it's too light for data)
|
|
24
|
+
COLOR_PALETTE = ["#15a8a8", "#fe5d26", "#bf1363", "#023d60"]
|
|
25
|
+
|
|
26
|
+
def generate_chart(self, df: pd.DataFrame, title: str = "Chart") -> Dict[str, Any]:
|
|
27
|
+
"""Generate a Plotly chart based on DataFrame shape and types.
|
|
28
|
+
|
|
29
|
+
Heuristics:
|
|
30
|
+
- 4+ columns: table
|
|
31
|
+
- 1 numeric column: histogram
|
|
32
|
+
- 2 columns (1 categorical, 1 numeric): bar chart
|
|
33
|
+
- 2 numeric columns: scatter plot
|
|
34
|
+
- 3+ numeric columns: correlation heatmap or multi-line chart
|
|
35
|
+
- Time series data: line chart
|
|
36
|
+
- Multiple categorical: grouped bar chart
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
df: DataFrame to visualize
|
|
40
|
+
title: Title for the chart
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Plotly figure as dictionary
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If DataFrame is empty or cannot be visualized
|
|
47
|
+
"""
|
|
48
|
+
if df.empty:
|
|
49
|
+
raise ValueError("Cannot visualize empty DataFrame")
|
|
50
|
+
|
|
51
|
+
# Heuristic: If 4 or more columns, render as a table
|
|
52
|
+
if len(df.columns) >= 4:
|
|
53
|
+
fig = self._create_table(df, title)
|
|
54
|
+
result: Dict[str, Any] = json.loads(pio.to_json(fig))
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
# Identify column types
|
|
58
|
+
numeric_cols = df.select_dtypes(include=["number"]).columns.tolist()
|
|
59
|
+
categorical_cols = df.select_dtypes(
|
|
60
|
+
include=["object", "category"]
|
|
61
|
+
).columns.tolist()
|
|
62
|
+
datetime_cols = df.select_dtypes(include=["datetime64"]).columns.tolist()
|
|
63
|
+
|
|
64
|
+
# Check for time series
|
|
65
|
+
is_timeseries = len(datetime_cols) > 0
|
|
66
|
+
|
|
67
|
+
# Apply heuristics
|
|
68
|
+
if is_timeseries and len(numeric_cols) > 0:
|
|
69
|
+
# Time series line chart
|
|
70
|
+
fig = self._create_time_series_chart(
|
|
71
|
+
df, datetime_cols[0], numeric_cols, title
|
|
72
|
+
)
|
|
73
|
+
elif len(numeric_cols) == 1 and len(categorical_cols) == 0:
|
|
74
|
+
# Single numeric column: histogram
|
|
75
|
+
fig = self._create_histogram(df, numeric_cols[0], title)
|
|
76
|
+
elif len(numeric_cols) == 1 and len(categorical_cols) == 1:
|
|
77
|
+
# One categorical, one numeric: bar chart
|
|
78
|
+
fig = self._create_bar_chart(
|
|
79
|
+
df, categorical_cols[0], numeric_cols[0], title
|
|
80
|
+
)
|
|
81
|
+
elif len(numeric_cols) == 2:
|
|
82
|
+
# Two numeric columns: scatter plot
|
|
83
|
+
fig = self._create_scatter_plot(df, numeric_cols[0], numeric_cols[1], title)
|
|
84
|
+
elif len(numeric_cols) >= 3:
|
|
85
|
+
# Multiple numeric columns: correlation heatmap
|
|
86
|
+
fig = self._create_correlation_heatmap(df, numeric_cols, title)
|
|
87
|
+
elif len(categorical_cols) >= 2:
|
|
88
|
+
# Multiple categorical: grouped bar chart
|
|
89
|
+
fig = self._create_grouped_bar_chart(df, categorical_cols, title)
|
|
90
|
+
else:
|
|
91
|
+
# Fallback: show first two columns as scatter/bar
|
|
92
|
+
if len(df.columns) >= 2:
|
|
93
|
+
fig = self._create_generic_chart(
|
|
94
|
+
df, df.columns[0], df.columns[1], title
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
"Cannot determine appropriate visualization for this DataFrame"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Convert to JSON-serializable dict using plotly's JSON encoder
|
|
102
|
+
result = json.loads(pio.to_json(fig))
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
def _apply_standard_layout(self, fig: go.Figure) -> go.Figure:
|
|
106
|
+
"""Apply consistent Vanna brand styling to all charts.
|
|
107
|
+
|
|
108
|
+
Uses Vanna brand colors from the landing page for a cohesive look.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
fig: Plotly figure to update
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Updated figure with Vanna brand styling
|
|
115
|
+
"""
|
|
116
|
+
fig.update_layout(
|
|
117
|
+
# paper_bgcolor='white',
|
|
118
|
+
# plot_bgcolor='white',
|
|
119
|
+
font={"color": self.THEME_COLORS["navy"]}, # Navy for text
|
|
120
|
+
autosize=True, # Allow chart to resize responsively
|
|
121
|
+
colorway=self.COLOR_PALETTE, # Use Vanna brand colors for data
|
|
122
|
+
# Don't set width/height - let frontend handle sizing
|
|
123
|
+
)
|
|
124
|
+
return fig
|
|
125
|
+
|
|
126
|
+
def _create_histogram(self, df: pd.DataFrame, column: str, title: str) -> go.Figure:
|
|
127
|
+
"""Create a histogram for a single numeric column."""
|
|
128
|
+
fig = px.histogram(
|
|
129
|
+
df,
|
|
130
|
+
x=column,
|
|
131
|
+
title=title,
|
|
132
|
+
color_discrete_sequence=[self.THEME_COLORS["teal"]],
|
|
133
|
+
)
|
|
134
|
+
fig.update_layout(xaxis_title=column, yaxis_title="Count", showlegend=False)
|
|
135
|
+
self._apply_standard_layout(fig)
|
|
136
|
+
return fig
|
|
137
|
+
|
|
138
|
+
def _create_bar_chart(
|
|
139
|
+
self, df: pd.DataFrame, x_col: str, y_col: str, title: str
|
|
140
|
+
) -> go.Figure:
|
|
141
|
+
"""Create a bar chart for categorical vs numeric data."""
|
|
142
|
+
# Aggregate if needed
|
|
143
|
+
agg_df = df.groupby(x_col)[y_col].sum().reset_index()
|
|
144
|
+
fig = px.bar(
|
|
145
|
+
agg_df,
|
|
146
|
+
x=x_col,
|
|
147
|
+
y=y_col,
|
|
148
|
+
title=title,
|
|
149
|
+
color_discrete_sequence=[self.THEME_COLORS["orange"]],
|
|
150
|
+
)
|
|
151
|
+
fig.update_layout(xaxis_title=x_col, yaxis_title=y_col)
|
|
152
|
+
self._apply_standard_layout(fig)
|
|
153
|
+
return fig
|
|
154
|
+
|
|
155
|
+
def _create_scatter_plot(
|
|
156
|
+
self, df: pd.DataFrame, x_col: str, y_col: str, title: str
|
|
157
|
+
) -> go.Figure:
|
|
158
|
+
"""Create a scatter plot for two numeric columns."""
|
|
159
|
+
fig = px.scatter(
|
|
160
|
+
df,
|
|
161
|
+
x=x_col,
|
|
162
|
+
y=y_col,
|
|
163
|
+
title=title,
|
|
164
|
+
color_discrete_sequence=[self.THEME_COLORS["magenta"]],
|
|
165
|
+
)
|
|
166
|
+
fig.update_layout(xaxis_title=x_col, yaxis_title=y_col)
|
|
167
|
+
self._apply_standard_layout(fig)
|
|
168
|
+
return fig
|
|
169
|
+
|
|
170
|
+
def _create_correlation_heatmap(
|
|
171
|
+
self, df: pd.DataFrame, columns: List[str], title: str
|
|
172
|
+
) -> go.Figure:
|
|
173
|
+
"""Create a correlation heatmap for multiple numeric columns."""
|
|
174
|
+
corr_matrix = df[columns].corr()
|
|
175
|
+
# Custom Vanna color scale: navy (negative) -> cream (neutral) -> teal (positive)
|
|
176
|
+
vanna_colorscale = [
|
|
177
|
+
[0.0, self.THEME_COLORS["navy"]],
|
|
178
|
+
[0.5, self.THEME_COLORS["cream"]],
|
|
179
|
+
[1.0, self.THEME_COLORS["teal"]],
|
|
180
|
+
]
|
|
181
|
+
fig = cast(
|
|
182
|
+
go.Figure,
|
|
183
|
+
px.imshow(
|
|
184
|
+
corr_matrix,
|
|
185
|
+
title=title,
|
|
186
|
+
labels=dict(color="Correlation"),
|
|
187
|
+
x=columns,
|
|
188
|
+
y=columns,
|
|
189
|
+
color_continuous_scale=vanna_colorscale,
|
|
190
|
+
zmin=-1,
|
|
191
|
+
zmax=1,
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
self._apply_standard_layout(fig)
|
|
195
|
+
return fig
|
|
196
|
+
|
|
197
|
+
def _create_time_series_chart(
|
|
198
|
+
self, df: pd.DataFrame, time_col: str, value_cols: List[str], title: str
|
|
199
|
+
) -> go.Figure:
|
|
200
|
+
"""Create a time series line chart."""
|
|
201
|
+
fig = go.Figure()
|
|
202
|
+
|
|
203
|
+
for i, col in enumerate(value_cols[:5]): # Limit to 5 lines for readability
|
|
204
|
+
color = self.COLOR_PALETTE[i % len(self.COLOR_PALETTE)]
|
|
205
|
+
fig.add_trace(
|
|
206
|
+
go.Scatter(
|
|
207
|
+
x=df[time_col],
|
|
208
|
+
y=df[col],
|
|
209
|
+
mode="lines",
|
|
210
|
+
name=col,
|
|
211
|
+
line=dict(color=color),
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
fig.update_layout(
|
|
216
|
+
title=title,
|
|
217
|
+
xaxis_title=time_col,
|
|
218
|
+
yaxis_title="Value",
|
|
219
|
+
hovermode="x unified",
|
|
220
|
+
)
|
|
221
|
+
self._apply_standard_layout(fig)
|
|
222
|
+
return fig
|
|
223
|
+
|
|
224
|
+
def _create_grouped_bar_chart(
|
|
225
|
+
self, df: pd.DataFrame, categorical_cols: List[str], title: str
|
|
226
|
+
) -> go.Figure:
|
|
227
|
+
"""Create a grouped bar chart for multiple categorical columns."""
|
|
228
|
+
# Use first two categorical columns
|
|
229
|
+
if len(categorical_cols) >= 2:
|
|
230
|
+
# Count occurrences
|
|
231
|
+
grouped = df.groupby(categorical_cols[:2]).size().reset_index(name="count")
|
|
232
|
+
fig = px.bar(
|
|
233
|
+
grouped,
|
|
234
|
+
x=categorical_cols[0],
|
|
235
|
+
y="count",
|
|
236
|
+
color=categorical_cols[1],
|
|
237
|
+
title=title,
|
|
238
|
+
barmode="group",
|
|
239
|
+
color_discrete_sequence=self.COLOR_PALETTE,
|
|
240
|
+
)
|
|
241
|
+
self._apply_standard_layout(fig)
|
|
242
|
+
return fig
|
|
243
|
+
else:
|
|
244
|
+
# Single categorical: value counts
|
|
245
|
+
counts = df[categorical_cols[0]].value_counts().reset_index()
|
|
246
|
+
counts.columns = [categorical_cols[0], "count"]
|
|
247
|
+
fig = px.bar(
|
|
248
|
+
counts,
|
|
249
|
+
x=categorical_cols[0],
|
|
250
|
+
y="count",
|
|
251
|
+
title=title,
|
|
252
|
+
color_discrete_sequence=[self.THEME_COLORS["teal"]],
|
|
253
|
+
)
|
|
254
|
+
self._apply_standard_layout(fig)
|
|
255
|
+
return fig
|
|
256
|
+
|
|
257
|
+
def _create_generic_chart(
|
|
258
|
+
self, df: pd.DataFrame, col1: str, col2: str, title: str
|
|
259
|
+
) -> go.Figure:
|
|
260
|
+
"""Create a generic chart for any two columns."""
|
|
261
|
+
# Try to determine the best representation
|
|
262
|
+
if pd.api.types.is_numeric_dtype(df[col1]) and pd.api.types.is_numeric_dtype(
|
|
263
|
+
df[col2]
|
|
264
|
+
):
|
|
265
|
+
return self._create_scatter_plot(df, col1, col2, title)
|
|
266
|
+
else:
|
|
267
|
+
# Treat first as categorical, second as value
|
|
268
|
+
fig = px.bar(
|
|
269
|
+
df,
|
|
270
|
+
x=col1,
|
|
271
|
+
y=col2,
|
|
272
|
+
title=title,
|
|
273
|
+
color_discrete_sequence=[self.THEME_COLORS["orange"]],
|
|
274
|
+
)
|
|
275
|
+
self._apply_standard_layout(fig)
|
|
276
|
+
return fig
|
|
277
|
+
|
|
278
|
+
def _create_table(self, df: pd.DataFrame, title: str) -> go.Figure:
|
|
279
|
+
"""Create a Plotly table for DataFrames with 4 or more columns."""
|
|
280
|
+
# Prepare header
|
|
281
|
+
header_values = list(df.columns)
|
|
282
|
+
|
|
283
|
+
# Prepare cell values (transpose to get columns)
|
|
284
|
+
cell_values = [df[col].tolist() for col in df.columns]
|
|
285
|
+
|
|
286
|
+
# Create the table
|
|
287
|
+
fig = go.Figure(
|
|
288
|
+
data=[
|
|
289
|
+
go.Table(
|
|
290
|
+
header=dict(
|
|
291
|
+
values=header_values,
|
|
292
|
+
fill_color=self.THEME_COLORS["navy"],
|
|
293
|
+
font=dict(color="white", size=12),
|
|
294
|
+
align="left",
|
|
295
|
+
),
|
|
296
|
+
cells=dict(
|
|
297
|
+
values=cell_values,
|
|
298
|
+
fill_color=[
|
|
299
|
+
[
|
|
300
|
+
self.THEME_COLORS["cream"] if i % 2 == 0 else "white"
|
|
301
|
+
for i in range(len(df))
|
|
302
|
+
]
|
|
303
|
+
],
|
|
304
|
+
font=dict(color=self.THEME_COLORS["navy"], size=11),
|
|
305
|
+
align="left",
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
]
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
fig.update_layout(title=title, font={"color": self.THEME_COLORS["navy"]})
|
|
312
|
+
|
|
313
|
+
return fig
|