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,285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Demo in-memory implementation of AgentMemory.
|
|
3
|
+
|
|
4
|
+
This implementation provides a zero-dependency, minimal storage solution that
|
|
5
|
+
keeps all memories in RAM. It uses simple similarity algorithms (Jaccard and
|
|
6
|
+
difflib) instead of vector embeddings. Perfect for demos and testing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import difflib
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from vanna.capabilities.agent_memory import (
|
|
19
|
+
AgentMemory,
|
|
20
|
+
TextMemory,
|
|
21
|
+
TextMemorySearchResult,
|
|
22
|
+
ToolMemory,
|
|
23
|
+
ToolMemorySearchResult,
|
|
24
|
+
)
|
|
25
|
+
from vanna.core.tool import ToolContext
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DemoAgentMemory(AgentMemory):
|
|
29
|
+
"""
|
|
30
|
+
Minimal, dependency-free in-memory storage for demos and testing.
|
|
31
|
+
- O(n) search over an in-memory list
|
|
32
|
+
- Simple similarity: max(Jaccard(token sets), difflib ratio)
|
|
33
|
+
- Optional FIFO eviction via max_items
|
|
34
|
+
- Async-safe with an asyncio.Lock
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, *, max_items: int = 10_000):
|
|
38
|
+
"""
|
|
39
|
+
Initialize the in-memory storage.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
max_items: Maximum number of memories to keep. Oldest memories are
|
|
43
|
+
evicted when this limit is reached (FIFO).
|
|
44
|
+
"""
|
|
45
|
+
self._memories: List[ToolMemory] = []
|
|
46
|
+
self._text_memories: List[TextMemory] = []
|
|
47
|
+
self._lock = asyncio.Lock()
|
|
48
|
+
self._max_items = max_items
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def _now_iso() -> str:
|
|
52
|
+
"""Get current timestamp in ISO format."""
|
|
53
|
+
return datetime.now().isoformat()
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _normalize(text: str) -> str:
|
|
57
|
+
"""Normalize text by lowercasing and collapsing whitespace."""
|
|
58
|
+
return " ".join(text.lower().split())
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _tokenize(text: str) -> set[str]:
|
|
62
|
+
"""Simple tokenizer that splits on whitespace."""
|
|
63
|
+
return set(text.lower().split())
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def _similarity(cls, a: str, b: str) -> float:
|
|
67
|
+
"""
|
|
68
|
+
Calculate similarity between two strings using multiple methods.
|
|
69
|
+
|
|
70
|
+
Returns the maximum of Jaccard similarity and difflib ratio.
|
|
71
|
+
"""
|
|
72
|
+
a_norm, b_norm = cls._normalize(a), cls._normalize(b)
|
|
73
|
+
|
|
74
|
+
# Jaccard over whitespace tokens
|
|
75
|
+
ta, tb = cls._tokenize(a_norm), cls._tokenize(b_norm)
|
|
76
|
+
if not ta and not tb:
|
|
77
|
+
jaccard = 1.0
|
|
78
|
+
elif not ta or not tb:
|
|
79
|
+
jaccard = 0.0
|
|
80
|
+
else:
|
|
81
|
+
jaccard = len(ta & tb) / max(1, len(ta | tb))
|
|
82
|
+
|
|
83
|
+
# difflib ratio
|
|
84
|
+
ratio = difflib.SequenceMatcher(None, a_norm, b_norm).ratio()
|
|
85
|
+
|
|
86
|
+
# Take the better of the two cheap measures
|
|
87
|
+
return max(jaccard, ratio)
|
|
88
|
+
|
|
89
|
+
async def save_tool_usage(
|
|
90
|
+
self,
|
|
91
|
+
question: str,
|
|
92
|
+
tool_name: str,
|
|
93
|
+
args: Dict[str, Any],
|
|
94
|
+
context: ToolContext,
|
|
95
|
+
success: bool = True,
|
|
96
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Save a tool usage pattern for future reference."""
|
|
99
|
+
tm = ToolMemory(
|
|
100
|
+
memory_id=str(uuid.uuid4()),
|
|
101
|
+
question=question,
|
|
102
|
+
tool_name=tool_name,
|
|
103
|
+
args=args,
|
|
104
|
+
timestamp=self._now_iso(),
|
|
105
|
+
success=success,
|
|
106
|
+
metadata=metadata or {},
|
|
107
|
+
)
|
|
108
|
+
async with self._lock:
|
|
109
|
+
self._memories.append(tm)
|
|
110
|
+
# Optional FIFO eviction
|
|
111
|
+
if len(self._memories) > self._max_items:
|
|
112
|
+
overflow = len(self._memories) - self._max_items
|
|
113
|
+
del self._memories[:overflow]
|
|
114
|
+
|
|
115
|
+
async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:
|
|
116
|
+
"""Store a text memory in RAM."""
|
|
117
|
+
tm = TextMemory(
|
|
118
|
+
memory_id=str(uuid.uuid4()), content=content, timestamp=self._now_iso()
|
|
119
|
+
)
|
|
120
|
+
async with self._lock:
|
|
121
|
+
self._text_memories.append(tm)
|
|
122
|
+
if len(self._text_memories) > self._max_items:
|
|
123
|
+
overflow = len(self._text_memories) - self._max_items
|
|
124
|
+
del self._text_memories[:overflow]
|
|
125
|
+
return tm
|
|
126
|
+
|
|
127
|
+
async def search_similar_usage(
|
|
128
|
+
self,
|
|
129
|
+
question: str,
|
|
130
|
+
context: ToolContext,
|
|
131
|
+
*,
|
|
132
|
+
limit: int = 10,
|
|
133
|
+
similarity_threshold: float = 0.7,
|
|
134
|
+
tool_name_filter: Optional[str] = None,
|
|
135
|
+
) -> List[ToolMemorySearchResult]:
|
|
136
|
+
"""Search for similar tool usage patterns based on a question."""
|
|
137
|
+
q = self._normalize(question)
|
|
138
|
+
|
|
139
|
+
async with self._lock:
|
|
140
|
+
# Filter candidates by tool name and success status
|
|
141
|
+
candidates = [
|
|
142
|
+
m
|
|
143
|
+
for m in self._memories
|
|
144
|
+
if m.success
|
|
145
|
+
and (tool_name_filter is None or m.tool_name == tool_name_filter)
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
# Score each candidate by question similarity
|
|
149
|
+
results: List[tuple[ToolMemory, float]] = []
|
|
150
|
+
for m in candidates:
|
|
151
|
+
score = self._similarity(q, m.question)
|
|
152
|
+
results.append((m, min(score, 1.0)))
|
|
153
|
+
|
|
154
|
+
# Filter by threshold and sort by score
|
|
155
|
+
results = [(m, s) for (m, s) in results if s >= similarity_threshold]
|
|
156
|
+
results.sort(key=lambda x: x[1], reverse=True)
|
|
157
|
+
|
|
158
|
+
# Build ranked response
|
|
159
|
+
out: List[ToolMemorySearchResult] = []
|
|
160
|
+
for idx, (m, s) in enumerate(results[:limit], start=1):
|
|
161
|
+
out.append(
|
|
162
|
+
ToolMemorySearchResult(memory=m, similarity_score=s, rank=idx)
|
|
163
|
+
)
|
|
164
|
+
return out
|
|
165
|
+
|
|
166
|
+
async def search_text_memories(
|
|
167
|
+
self,
|
|
168
|
+
query: str,
|
|
169
|
+
context: ToolContext,
|
|
170
|
+
*,
|
|
171
|
+
limit: int = 10,
|
|
172
|
+
similarity_threshold: float = 0.7,
|
|
173
|
+
) -> List[TextMemorySearchResult]:
|
|
174
|
+
"""Search free-form text memories using the demo similarity metric."""
|
|
175
|
+
normalized_query = self._normalize(query)
|
|
176
|
+
|
|
177
|
+
async with self._lock:
|
|
178
|
+
scored: List[tuple[TextMemory, float]] = []
|
|
179
|
+
for memory in self._text_memories:
|
|
180
|
+
score = self._similarity(normalized_query, memory.content)
|
|
181
|
+
scored.append((memory, min(score, 1.0)))
|
|
182
|
+
|
|
183
|
+
scored = [
|
|
184
|
+
(memory, score)
|
|
185
|
+
for memory, score in scored
|
|
186
|
+
if score >= similarity_threshold
|
|
187
|
+
]
|
|
188
|
+
scored.sort(key=lambda item: item[1], reverse=True)
|
|
189
|
+
|
|
190
|
+
results: List[TextMemorySearchResult] = []
|
|
191
|
+
for idx, (memory, score) in enumerate(scored[:limit], start=1):
|
|
192
|
+
results.append(
|
|
193
|
+
TextMemorySearchResult(
|
|
194
|
+
memory=memory, similarity_score=score, rank=idx
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
return results
|
|
198
|
+
|
|
199
|
+
async def get_recent_memories(
|
|
200
|
+
self, context: ToolContext, limit: int = 10
|
|
201
|
+
) -> List[ToolMemory]:
|
|
202
|
+
"""Get recently added memories. Returns most recent memories first."""
|
|
203
|
+
async with self._lock:
|
|
204
|
+
# Return memories in reverse order (most recent first)
|
|
205
|
+
return list(reversed(self._memories[-limit:]))
|
|
206
|
+
|
|
207
|
+
async def get_recent_text_memories(
|
|
208
|
+
self, context: ToolContext, limit: int = 10
|
|
209
|
+
) -> List[TextMemory]:
|
|
210
|
+
"""Return recently added text memories."""
|
|
211
|
+
async with self._lock:
|
|
212
|
+
return list(reversed(self._text_memories[-limit:]))
|
|
213
|
+
|
|
214
|
+
async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:
|
|
215
|
+
"""Delete a stored text memory by ID."""
|
|
216
|
+
async with self._lock:
|
|
217
|
+
for index, memory in enumerate(self._text_memories):
|
|
218
|
+
if memory.memory_id == memory_id:
|
|
219
|
+
del self._text_memories[index]
|
|
220
|
+
return True
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:
|
|
224
|
+
"""Delete a memory by its ID. Returns True if deleted, False if not found."""
|
|
225
|
+
async with self._lock:
|
|
226
|
+
for i, m in enumerate(self._memories):
|
|
227
|
+
if m.memory_id == memory_id:
|
|
228
|
+
del self._memories[i]
|
|
229
|
+
return True
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
async def clear_memories(
|
|
233
|
+
self,
|
|
234
|
+
context: ToolContext,
|
|
235
|
+
tool_name: Optional[str] = None,
|
|
236
|
+
before_date: Optional[str] = None,
|
|
237
|
+
) -> int:
|
|
238
|
+
"""Clear stored memories. Returns number of memories deleted."""
|
|
239
|
+
async with self._lock:
|
|
240
|
+
original_tool_count = len(self._memories)
|
|
241
|
+
original_text_count = len(self._text_memories)
|
|
242
|
+
|
|
243
|
+
# Filter memories to keep
|
|
244
|
+
kept_memories = []
|
|
245
|
+
for m in self._memories:
|
|
246
|
+
should_delete = True
|
|
247
|
+
|
|
248
|
+
# Check tool name filter
|
|
249
|
+
if tool_name and m.tool_name != tool_name:
|
|
250
|
+
should_delete = False
|
|
251
|
+
|
|
252
|
+
# Check date filter
|
|
253
|
+
if should_delete and before_date and m.timestamp:
|
|
254
|
+
if m.timestamp >= before_date:
|
|
255
|
+
should_delete = False
|
|
256
|
+
|
|
257
|
+
# If no filters specified, delete all
|
|
258
|
+
if tool_name is None and before_date is None:
|
|
259
|
+
should_delete = True
|
|
260
|
+
|
|
261
|
+
# Keep if should not delete
|
|
262
|
+
if not should_delete:
|
|
263
|
+
kept_memories.append(m)
|
|
264
|
+
|
|
265
|
+
self._memories = kept_memories
|
|
266
|
+
deleted_tool_count = original_tool_count - len(self._memories)
|
|
267
|
+
|
|
268
|
+
# Apply filters to text memories (tool filter ignored)
|
|
269
|
+
kept_text_memories = []
|
|
270
|
+
for memory in self._text_memories:
|
|
271
|
+
should_delete = (
|
|
272
|
+
tool_name is None
|
|
273
|
+
) # only delete text when not targeting a tool
|
|
274
|
+
|
|
275
|
+
if before_date and memory.timestamp:
|
|
276
|
+
if memory.timestamp >= before_date:
|
|
277
|
+
should_delete = False
|
|
278
|
+
|
|
279
|
+
if not should_delete:
|
|
280
|
+
kept_text_memories.append(memory)
|
|
281
|
+
|
|
282
|
+
self._text_memories = kept_text_memories
|
|
283
|
+
deleted_text_count = original_text_count - len(self._text_memories)
|
|
284
|
+
|
|
285
|
+
return deleted_tool_count + deleted_text_count
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local audit logger implementation using Python logging.
|
|
3
|
+
|
|
4
|
+
This module provides a simple audit logger that writes events using
|
|
5
|
+
the standard Python logging module, useful for development and testing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from vanna.core.audit import AuditEvent, AuditLogger
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LoggingAuditLogger(AuditLogger):
|
|
18
|
+
"""Audit logger that writes events to Python logger as structured JSON.
|
|
19
|
+
|
|
20
|
+
This implementation uses logger.info() to emit audit events as JSON,
|
|
21
|
+
making them easy to parse and route to log aggregation systems.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
audit_logger = LoggingAuditLogger()
|
|
25
|
+
agent = Agent(
|
|
26
|
+
llm_service=...,
|
|
27
|
+
audit_logger=audit_logger
|
|
28
|
+
)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, log_level: int = logging.INFO):
|
|
32
|
+
"""Initialize the logging audit logger.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
log_level: Log level to use for audit events (default: INFO)
|
|
36
|
+
"""
|
|
37
|
+
self.log_level = log_level
|
|
38
|
+
|
|
39
|
+
async def log_event(self, event: AuditEvent) -> None:
|
|
40
|
+
"""Log an audit event as structured JSON.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
event: The audit event to log
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
# Convert event to dict for JSON serialization
|
|
47
|
+
event_dict = event.model_dump(mode="json", exclude_none=True)
|
|
48
|
+
|
|
49
|
+
# Format as single-line JSON for easy parsing
|
|
50
|
+
event_json = json.dumps(event_dict, separators=(",", ":"))
|
|
51
|
+
|
|
52
|
+
# Log with structured prefix for easy filtering
|
|
53
|
+
logger.log(
|
|
54
|
+
self.log_level,
|
|
55
|
+
f"[AUDIT] {event.event_type.value} | {event_json}",
|
|
56
|
+
)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
# Don't fail the operation if audit logging fails
|
|
59
|
+
logger.error(f"Failed to log audit event: {e}", exc_info=True)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local file system implementation.
|
|
3
|
+
|
|
4
|
+
This module provides a local file system implementation with per-user isolation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import hashlib
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from vanna.capabilities.file_system import CommandResult, FileSearchMatch, FileSystem
|
|
13
|
+
from vanna.core.tool import ToolContext
|
|
14
|
+
|
|
15
|
+
MAX_SEARCH_FILE_BYTES = 1_000_000
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LocalFileSystem(FileSystem):
|
|
19
|
+
"""Local file system implementation with per-user isolation."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, working_directory: str = "."):
|
|
22
|
+
"""Initialize with a working directory.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
working_directory: Base directory where user-specific folders will be created
|
|
26
|
+
"""
|
|
27
|
+
self.working_directory = Path(working_directory)
|
|
28
|
+
|
|
29
|
+
def _get_user_directory(self, context: ToolContext) -> Path:
|
|
30
|
+
"""Get the user-specific directory by hashing the user ID.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
context: Tool context containing user information
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Path to the user-specific directory
|
|
37
|
+
"""
|
|
38
|
+
# Hash the user ID to create a directory name
|
|
39
|
+
user_hash = hashlib.sha256(context.user.id.encode()).hexdigest()[:16]
|
|
40
|
+
user_dir = self.working_directory / user_hash
|
|
41
|
+
|
|
42
|
+
# Create the directory if it doesn't exist
|
|
43
|
+
user_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
|
|
45
|
+
return user_dir
|
|
46
|
+
|
|
47
|
+
def _resolve_path(self, path: str, context: ToolContext) -> Path:
|
|
48
|
+
"""Resolve a path relative to the user's directory.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
path: Path relative to user directory
|
|
52
|
+
context: Tool context containing user information
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Absolute path within user's directory
|
|
56
|
+
"""
|
|
57
|
+
user_dir = self._get_user_directory(context)
|
|
58
|
+
resolved = user_dir / path
|
|
59
|
+
|
|
60
|
+
# Ensure the path is within the user's directory (prevent directory traversal)
|
|
61
|
+
try:
|
|
62
|
+
resolved.resolve().relative_to(user_dir.resolve())
|
|
63
|
+
except ValueError:
|
|
64
|
+
raise PermissionError(
|
|
65
|
+
f"Access denied: path '{path}' is outside user directory"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return resolved
|
|
69
|
+
|
|
70
|
+
async def list_files(self, directory: str, context: ToolContext) -> List[str]:
|
|
71
|
+
"""List files in a directory within the user's isolated space."""
|
|
72
|
+
directory_path = self._resolve_path(directory, context)
|
|
73
|
+
|
|
74
|
+
if not directory_path.exists():
|
|
75
|
+
raise FileNotFoundError(f"Directory '{directory}' does not exist")
|
|
76
|
+
|
|
77
|
+
if not directory_path.is_dir():
|
|
78
|
+
raise NotADirectoryError(f"'{directory}' is not a directory")
|
|
79
|
+
|
|
80
|
+
files = []
|
|
81
|
+
for item in directory_path.iterdir():
|
|
82
|
+
if item.is_file():
|
|
83
|
+
files.append(item.name)
|
|
84
|
+
|
|
85
|
+
return sorted(files)
|
|
86
|
+
|
|
87
|
+
async def read_file(self, filename: str, context: ToolContext) -> str:
|
|
88
|
+
"""Read the contents of a file within the user's isolated space."""
|
|
89
|
+
file_path = self._resolve_path(filename, context)
|
|
90
|
+
|
|
91
|
+
if not file_path.exists():
|
|
92
|
+
raise FileNotFoundError(f"File '{filename}' does not exist")
|
|
93
|
+
|
|
94
|
+
if not file_path.is_file():
|
|
95
|
+
raise IsADirectoryError(f"'{filename}' is a directory, not a file")
|
|
96
|
+
|
|
97
|
+
return file_path.read_text(encoding="utf-8")
|
|
98
|
+
|
|
99
|
+
async def write_file(
|
|
100
|
+
self, filename: str, content: str, context: ToolContext, overwrite: bool = False
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Write content to a file within the user's isolated space."""
|
|
103
|
+
file_path = self._resolve_path(filename, context)
|
|
104
|
+
|
|
105
|
+
# Create parent directories if they don't exist
|
|
106
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
|
|
108
|
+
if file_path.exists() and not overwrite:
|
|
109
|
+
raise FileExistsError(
|
|
110
|
+
f"File '{filename}' already exists. Use overwrite=True to replace it."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
file_path.write_text(content, encoding="utf-8")
|
|
114
|
+
|
|
115
|
+
async def exists(self, path: str, context: ToolContext) -> bool:
|
|
116
|
+
"""Check if a file or directory exists within the user's isolated space."""
|
|
117
|
+
try:
|
|
118
|
+
resolved_path = self._resolve_path(path, context)
|
|
119
|
+
return resolved_path.exists()
|
|
120
|
+
except PermissionError:
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
async def is_directory(self, path: str, context: ToolContext) -> bool:
|
|
124
|
+
"""Check if a path is a directory within the user's isolated space."""
|
|
125
|
+
try:
|
|
126
|
+
resolved_path = self._resolve_path(path, context)
|
|
127
|
+
return resolved_path.exists() and resolved_path.is_dir()
|
|
128
|
+
except PermissionError:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
async def search_files(
|
|
132
|
+
self,
|
|
133
|
+
query: str,
|
|
134
|
+
context: ToolContext,
|
|
135
|
+
*,
|
|
136
|
+
max_results: int = 20,
|
|
137
|
+
include_content: bool = False,
|
|
138
|
+
) -> List[FileSearchMatch]:
|
|
139
|
+
"""Search for files within the user's isolated space."""
|
|
140
|
+
|
|
141
|
+
trimmed_query = query.strip()
|
|
142
|
+
if not trimmed_query:
|
|
143
|
+
raise ValueError("Search query must not be empty")
|
|
144
|
+
|
|
145
|
+
user_dir = self._get_user_directory(context)
|
|
146
|
+
matches: List[FileSearchMatch] = []
|
|
147
|
+
query_lower = trimmed_query.lower()
|
|
148
|
+
|
|
149
|
+
for path in user_dir.rglob("*"):
|
|
150
|
+
if len(matches) >= max_results:
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
if not path.is_file():
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
relative_path = path.relative_to(user_dir).as_posix()
|
|
157
|
+
include_entry = False
|
|
158
|
+
snippet: Optional[str] = None
|
|
159
|
+
|
|
160
|
+
if query_lower in path.name.lower():
|
|
161
|
+
include_entry = True
|
|
162
|
+
snippet = "[filename match]"
|
|
163
|
+
|
|
164
|
+
content: Optional[str] = None
|
|
165
|
+
if include_content:
|
|
166
|
+
try:
|
|
167
|
+
size = path.stat().st_size
|
|
168
|
+
except OSError:
|
|
169
|
+
if include_entry:
|
|
170
|
+
matches.append(
|
|
171
|
+
FileSearchMatch(path=relative_path, snippet=snippet)
|
|
172
|
+
)
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
if size <= MAX_SEARCH_FILE_BYTES:
|
|
176
|
+
try:
|
|
177
|
+
content = path.read_text(encoding="utf-8")
|
|
178
|
+
except (UnicodeDecodeError, OSError):
|
|
179
|
+
content = None
|
|
180
|
+
elif not include_entry:
|
|
181
|
+
# Skip oversized files if they do not match by name
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
if include_content and content is not None:
|
|
185
|
+
if query_lower in content.lower():
|
|
186
|
+
# Create snippet
|
|
187
|
+
lowered = content.lower()
|
|
188
|
+
index = lowered.find(query_lower)
|
|
189
|
+
if index != -1:
|
|
190
|
+
context_window = 60
|
|
191
|
+
start = max(0, index - context_window)
|
|
192
|
+
end = min(len(content), index + len(query) + context_window)
|
|
193
|
+
snippet = content[start:end].replace("\n", " ").strip()
|
|
194
|
+
if start > 0:
|
|
195
|
+
snippet = f"…{snippet}"
|
|
196
|
+
if end < len(content):
|
|
197
|
+
snippet = f"{snippet}…"
|
|
198
|
+
include_entry = True
|
|
199
|
+
elif not include_entry:
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
if include_entry:
|
|
203
|
+
matches.append(FileSearchMatch(path=relative_path, snippet=snippet))
|
|
204
|
+
|
|
205
|
+
return matches
|
|
206
|
+
|
|
207
|
+
async def run_bash(
|
|
208
|
+
self,
|
|
209
|
+
command: str,
|
|
210
|
+
context: ToolContext,
|
|
211
|
+
*,
|
|
212
|
+
timeout: Optional[float] = None,
|
|
213
|
+
) -> CommandResult:
|
|
214
|
+
"""Execute a bash command within the user's isolated space."""
|
|
215
|
+
|
|
216
|
+
if not command.strip():
|
|
217
|
+
raise ValueError("Command must not be empty")
|
|
218
|
+
|
|
219
|
+
user_dir = self._get_user_directory(context)
|
|
220
|
+
|
|
221
|
+
process = await asyncio.create_subprocess_shell(
|
|
222
|
+
command,
|
|
223
|
+
stdout=asyncio.subprocess.PIPE,
|
|
224
|
+
stderr=asyncio.subprocess.PIPE,
|
|
225
|
+
cwd=str(user_dir),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
230
|
+
process.communicate(), timeout=timeout
|
|
231
|
+
)
|
|
232
|
+
except asyncio.TimeoutError as exc:
|
|
233
|
+
process.kill()
|
|
234
|
+
await process.wait()
|
|
235
|
+
raise TimeoutError(f"Command timed out after {timeout} seconds") from exc
|
|
236
|
+
|
|
237
|
+
stdout = stdout_bytes.decode("utf-8", errors="replace")
|
|
238
|
+
stderr = stderr_bytes.decode("utf-8", errors="replace")
|
|
239
|
+
|
|
240
|
+
return CommandResult(
|
|
241
|
+
stdout=stdout, stderr=stderr, returncode=process.returncode or 0
|
|
242
|
+
)
|