vanna 0.7.9__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. vanna/__init__.py +167 -395
  2. vanna/agents/__init__.py +7 -0
  3. vanna/capabilities/__init__.py +17 -0
  4. vanna/capabilities/agent_memory/__init__.py +21 -0
  5. vanna/capabilities/agent_memory/base.py +103 -0
  6. vanna/capabilities/agent_memory/models.py +53 -0
  7. vanna/capabilities/file_system/__init__.py +14 -0
  8. vanna/capabilities/file_system/base.py +71 -0
  9. vanna/capabilities/file_system/models.py +25 -0
  10. vanna/capabilities/sql_runner/__init__.py +13 -0
  11. vanna/capabilities/sql_runner/base.py +37 -0
  12. vanna/capabilities/sql_runner/models.py +13 -0
  13. vanna/components/__init__.py +92 -0
  14. vanna/components/base.py +11 -0
  15. vanna/components/rich/__init__.py +83 -0
  16. vanna/components/rich/containers/__init__.py +7 -0
  17. vanna/components/rich/containers/card.py +20 -0
  18. vanna/components/rich/data/__init__.py +9 -0
  19. vanna/components/rich/data/chart.py +17 -0
  20. vanna/components/rich/data/dataframe.py +93 -0
  21. vanna/components/rich/feedback/__init__.py +21 -0
  22. vanna/components/rich/feedback/badge.py +16 -0
  23. vanna/components/rich/feedback/icon_text.py +14 -0
  24. vanna/components/rich/feedback/log_viewer.py +41 -0
  25. vanna/components/rich/feedback/notification.py +19 -0
  26. vanna/components/rich/feedback/progress.py +37 -0
  27. vanna/components/rich/feedback/status_card.py +28 -0
  28. vanna/components/rich/feedback/status_indicator.py +14 -0
  29. vanna/components/rich/interactive/__init__.py +21 -0
  30. vanna/components/rich/interactive/button.py +95 -0
  31. vanna/components/rich/interactive/task_list.py +58 -0
  32. vanna/components/rich/interactive/ui_state.py +93 -0
  33. vanna/components/rich/specialized/__init__.py +7 -0
  34. vanna/components/rich/specialized/artifact.py +20 -0
  35. vanna/components/rich/text.py +16 -0
  36. vanna/components/simple/__init__.py +15 -0
  37. vanna/components/simple/image.py +15 -0
  38. vanna/components/simple/link.py +15 -0
  39. vanna/components/simple/text.py +11 -0
  40. vanna/core/__init__.py +193 -0
  41. vanna/core/_compat.py +19 -0
  42. vanna/core/agent/__init__.py +10 -0
  43. vanna/core/agent/agent.py +1407 -0
  44. vanna/core/agent/config.py +123 -0
  45. vanna/core/audit/__init__.py +28 -0
  46. vanna/core/audit/base.py +299 -0
  47. vanna/core/audit/models.py +131 -0
  48. vanna/core/component_manager.py +329 -0
  49. vanna/core/components.py +53 -0
  50. vanna/core/enhancer/__init__.py +11 -0
  51. vanna/core/enhancer/base.py +94 -0
  52. vanna/core/enhancer/default.py +118 -0
  53. vanna/core/enricher/__init__.py +10 -0
  54. vanna/core/enricher/base.py +59 -0
  55. vanna/core/errors.py +47 -0
  56. vanna/core/evaluation/__init__.py +81 -0
  57. vanna/core/evaluation/base.py +186 -0
  58. vanna/core/evaluation/dataset.py +254 -0
  59. vanna/core/evaluation/evaluators.py +376 -0
  60. vanna/core/evaluation/report.py +289 -0
  61. vanna/core/evaluation/runner.py +313 -0
  62. vanna/core/filter/__init__.py +10 -0
  63. vanna/core/filter/base.py +67 -0
  64. vanna/core/lifecycle/__init__.py +10 -0
  65. vanna/core/lifecycle/base.py +83 -0
  66. vanna/core/llm/__init__.py +16 -0
  67. vanna/core/llm/base.py +40 -0
  68. vanna/core/llm/models.py +61 -0
  69. vanna/core/middleware/__init__.py +10 -0
  70. vanna/core/middleware/base.py +69 -0
  71. vanna/core/observability/__init__.py +11 -0
  72. vanna/core/observability/base.py +88 -0
  73. vanna/core/observability/models.py +47 -0
  74. vanna/core/recovery/__init__.py +11 -0
  75. vanna/core/recovery/base.py +84 -0
  76. vanna/core/recovery/models.py +32 -0
  77. vanna/core/registry.py +278 -0
  78. vanna/core/rich_component.py +156 -0
  79. vanna/core/simple_component.py +27 -0
  80. vanna/core/storage/__init__.py +14 -0
  81. vanna/core/storage/base.py +46 -0
  82. vanna/core/storage/models.py +46 -0
  83. vanna/core/system_prompt/__init__.py +13 -0
  84. vanna/core/system_prompt/base.py +36 -0
  85. vanna/core/system_prompt/default.py +157 -0
  86. vanna/core/tool/__init__.py +18 -0
  87. vanna/core/tool/base.py +70 -0
  88. vanna/core/tool/models.py +84 -0
  89. vanna/core/user/__init__.py +17 -0
  90. vanna/core/user/base.py +29 -0
  91. vanna/core/user/models.py +25 -0
  92. vanna/core/user/request_context.py +70 -0
  93. vanna/core/user/resolver.py +42 -0
  94. vanna/core/validation.py +164 -0
  95. vanna/core/workflow/__init__.py +12 -0
  96. vanna/core/workflow/base.py +254 -0
  97. vanna/core/workflow/default.py +789 -0
  98. vanna/examples/__init__.py +1 -0
  99. vanna/examples/__main__.py +44 -0
  100. vanna/examples/anthropic_quickstart.py +80 -0
  101. vanna/examples/artifact_example.py +293 -0
  102. vanna/examples/claude_sqlite_example.py +236 -0
  103. vanna/examples/coding_agent_example.py +300 -0
  104. vanna/examples/custom_system_prompt_example.py +174 -0
  105. vanna/examples/default_workflow_handler_example.py +208 -0
  106. vanna/examples/email_auth_example.py +340 -0
  107. vanna/examples/evaluation_example.py +269 -0
  108. vanna/examples/extensibility_example.py +262 -0
  109. vanna/examples/minimal_example.py +67 -0
  110. vanna/examples/mock_auth_example.py +227 -0
  111. vanna/examples/mock_custom_tool.py +311 -0
  112. vanna/examples/mock_quickstart.py +79 -0
  113. vanna/examples/mock_quota_example.py +145 -0
  114. vanna/examples/mock_rich_components_demo.py +396 -0
  115. vanna/examples/mock_sqlite_example.py +223 -0
  116. vanna/examples/openai_quickstart.py +83 -0
  117. vanna/examples/primitive_components_demo.py +305 -0
  118. vanna/examples/quota_lifecycle_example.py +139 -0
  119. vanna/examples/visualization_example.py +251 -0
  120. vanna/integrations/__init__.py +17 -0
  121. vanna/integrations/anthropic/__init__.py +9 -0
  122. vanna/integrations/anthropic/llm.py +270 -0
  123. vanna/integrations/azureopenai/__init__.py +9 -0
  124. vanna/integrations/azureopenai/llm.py +329 -0
  125. vanna/integrations/azuresearch/__init__.py +7 -0
  126. vanna/integrations/azuresearch/agent_memory.py +413 -0
  127. vanna/integrations/bigquery/__init__.py +5 -0
  128. vanna/integrations/bigquery/sql_runner.py +81 -0
  129. vanna/integrations/chromadb/__init__.py +104 -0
  130. vanna/integrations/chromadb/agent_memory.py +416 -0
  131. vanna/integrations/clickhouse/__init__.py +5 -0
  132. vanna/integrations/clickhouse/sql_runner.py +82 -0
  133. vanna/integrations/duckdb/__init__.py +5 -0
  134. vanna/integrations/duckdb/sql_runner.py +65 -0
  135. vanna/integrations/faiss/__init__.py +7 -0
  136. vanna/integrations/faiss/agent_memory.py +431 -0
  137. vanna/integrations/google/__init__.py +9 -0
  138. vanna/integrations/google/gemini.py +370 -0
  139. vanna/integrations/hive/__init__.py +5 -0
  140. vanna/integrations/hive/sql_runner.py +87 -0
  141. vanna/integrations/local/__init__.py +17 -0
  142. vanna/integrations/local/agent_memory/__init__.py +7 -0
  143. vanna/integrations/local/agent_memory/in_memory.py +285 -0
  144. vanna/integrations/local/audit.py +59 -0
  145. vanna/integrations/local/file_system.py +242 -0
  146. vanna/integrations/local/file_system_conversation_store.py +255 -0
  147. vanna/integrations/local/storage.py +62 -0
  148. vanna/integrations/marqo/__init__.py +7 -0
  149. vanna/integrations/marqo/agent_memory.py +354 -0
  150. vanna/integrations/milvus/__init__.py +7 -0
  151. vanna/integrations/milvus/agent_memory.py +458 -0
  152. vanna/integrations/mock/__init__.py +9 -0
  153. vanna/integrations/mock/llm.py +65 -0
  154. vanna/integrations/mssql/__init__.py +5 -0
  155. vanna/integrations/mssql/sql_runner.py +66 -0
  156. vanna/integrations/mysql/__init__.py +5 -0
  157. vanna/integrations/mysql/sql_runner.py +92 -0
  158. vanna/integrations/ollama/__init__.py +7 -0
  159. vanna/integrations/ollama/llm.py +252 -0
  160. vanna/integrations/openai/__init__.py +10 -0
  161. vanna/integrations/openai/llm.py +267 -0
  162. vanna/integrations/openai/responses.py +163 -0
  163. vanna/integrations/opensearch/__init__.py +7 -0
  164. vanna/integrations/opensearch/agent_memory.py +411 -0
  165. vanna/integrations/oracle/__init__.py +5 -0
  166. vanna/integrations/oracle/sql_runner.py +75 -0
  167. vanna/integrations/pinecone/__init__.py +7 -0
  168. vanna/integrations/pinecone/agent_memory.py +329 -0
  169. vanna/integrations/plotly/__init__.py +5 -0
  170. vanna/integrations/plotly/chart_generator.py +313 -0
  171. vanna/integrations/postgres/__init__.py +9 -0
  172. vanna/integrations/postgres/sql_runner.py +112 -0
  173. vanna/integrations/premium/agent_memory/__init__.py +7 -0
  174. vanna/integrations/premium/agent_memory/premium.py +186 -0
  175. vanna/integrations/presto/__init__.py +5 -0
  176. vanna/integrations/presto/sql_runner.py +107 -0
  177. vanna/integrations/qdrant/__init__.py +7 -0
  178. vanna/integrations/qdrant/agent_memory.py +461 -0
  179. vanna/integrations/snowflake/__init__.py +5 -0
  180. vanna/integrations/snowflake/sql_runner.py +147 -0
  181. vanna/integrations/sqlite/__init__.py +9 -0
  182. vanna/integrations/sqlite/sql_runner.py +65 -0
  183. vanna/integrations/weaviate/__init__.py +7 -0
  184. vanna/integrations/weaviate/agent_memory.py +428 -0
  185. vanna/{ZhipuAI → legacy/ZhipuAI}/ZhipuAI_embeddings.py +11 -11
  186. vanna/legacy/__init__.py +403 -0
  187. vanna/legacy/adapter.py +463 -0
  188. vanna/{advanced → legacy/advanced}/__init__.py +3 -1
  189. vanna/{anthropic → legacy/anthropic}/anthropic_chat.py +9 -7
  190. vanna/{azuresearch → legacy/azuresearch}/azuresearch_vector.py +79 -41
  191. vanna/{base → legacy/base}/base.py +224 -217
  192. vanna/legacy/bedrock/__init__.py +1 -0
  193. vanna/{bedrock → legacy/bedrock}/bedrock_converse.py +13 -12
  194. vanna/{chromadb → legacy/chromadb}/chromadb_vector.py +3 -1
  195. vanna/legacy/cohere/__init__.py +2 -0
  196. vanna/{cohere → legacy/cohere}/cohere_chat.py +19 -14
  197. vanna/{cohere → legacy/cohere}/cohere_embeddings.py +25 -19
  198. vanna/{deepseek → legacy/deepseek}/deepseek_chat.py +5 -6
  199. vanna/legacy/faiss/__init__.py +1 -0
  200. vanna/{faiss → legacy/faiss}/faiss.py +113 -59
  201. vanna/{flask → legacy/flask}/__init__.py +84 -43
  202. vanna/{flask → legacy/flask}/assets.py +5 -5
  203. vanna/{flask → legacy/flask}/auth.py +5 -4
  204. vanna/{google → legacy/google}/bigquery_vector.py +75 -42
  205. vanna/{google → legacy/google}/gemini_chat.py +7 -3
  206. vanna/{hf → legacy/hf}/hf.py +0 -1
  207. vanna/{milvus → legacy/milvus}/milvus_vector.py +58 -35
  208. vanna/{mock → legacy/mock}/llm.py +0 -1
  209. vanna/legacy/mock/vectordb.py +67 -0
  210. vanna/legacy/ollama/ollama.py +110 -0
  211. vanna/{openai → legacy/openai}/openai_chat.py +2 -6
  212. vanna/legacy/opensearch/opensearch_vector.py +369 -0
  213. vanna/legacy/opensearch/opensearch_vector_semantic.py +200 -0
  214. vanna/legacy/oracle/oracle_vector.py +584 -0
  215. vanna/{pgvector → legacy/pgvector}/pgvector.py +42 -13
  216. vanna/{qdrant → legacy/qdrant}/qdrant.py +2 -6
  217. vanna/legacy/qianfan/Qianfan_Chat.py +170 -0
  218. vanna/legacy/qianfan/Qianfan_embeddings.py +36 -0
  219. vanna/legacy/qianwen/QianwenAI_chat.py +132 -0
  220. vanna/{remote.py → legacy/remote.py} +28 -26
  221. vanna/{utils.py → legacy/utils.py} +6 -11
  222. vanna/{vannadb → legacy/vannadb}/vannadb_vector.py +115 -46
  223. vanna/{vllm → legacy/vllm}/vllm.py +5 -6
  224. vanna/{weaviate → legacy/weaviate}/weaviate_vector.py +59 -40
  225. vanna/{xinference → legacy/xinference}/xinference.py +6 -6
  226. vanna/py.typed +0 -0
  227. vanna/servers/__init__.py +16 -0
  228. vanna/servers/__main__.py +8 -0
  229. vanna/servers/base/__init__.py +18 -0
  230. vanna/servers/base/chat_handler.py +65 -0
  231. vanna/servers/base/models.py +111 -0
  232. vanna/servers/base/rich_chat_handler.py +141 -0
  233. vanna/servers/base/templates.py +331 -0
  234. vanna/servers/cli/__init__.py +7 -0
  235. vanna/servers/cli/server_runner.py +204 -0
  236. vanna/servers/fastapi/__init__.py +7 -0
  237. vanna/servers/fastapi/app.py +163 -0
  238. vanna/servers/fastapi/routes.py +183 -0
  239. vanna/servers/flask/__init__.py +7 -0
  240. vanna/servers/flask/app.py +132 -0
  241. vanna/servers/flask/routes.py +137 -0
  242. vanna/tools/__init__.py +41 -0
  243. vanna/tools/agent_memory.py +322 -0
  244. vanna/tools/file_system.py +879 -0
  245. vanna/tools/python.py +222 -0
  246. vanna/tools/run_sql.py +165 -0
  247. vanna/tools/visualize_data.py +195 -0
  248. vanna/utils/__init__.py +0 -0
  249. vanna/web_components/__init__.py +44 -0
  250. vanna-2.0.0.dist-info/METADATA +485 -0
  251. vanna-2.0.0.dist-info/RECORD +289 -0
  252. vanna-2.0.0.dist-info/entry_points.txt +3 -0
  253. vanna/bedrock/__init__.py +0 -1
  254. vanna/cohere/__init__.py +0 -2
  255. vanna/faiss/__init__.py +0 -1
  256. vanna/mock/vectordb.py +0 -55
  257. vanna/ollama/ollama.py +0 -103
  258. vanna/opensearch/opensearch_vector.py +0 -392
  259. vanna/opensearch/opensearch_vector_semantic.py +0 -175
  260. vanna/oracle/oracle_vector.py +0 -585
  261. vanna/qianfan/Qianfan_Chat.py +0 -165
  262. vanna/qianfan/Qianfan_embeddings.py +0 -36
  263. vanna/qianwen/QianwenAI_chat.py +0 -133
  264. vanna-0.7.9.dist-info/METADATA +0 -408
  265. vanna-0.7.9.dist-info/RECORD +0 -79
  266. /vanna/{ZhipuAI → legacy/ZhipuAI}/ZhipuAI_Chat.py +0 -0
  267. /vanna/{ZhipuAI → legacy/ZhipuAI}/__init__.py +0 -0
  268. /vanna/{anthropic → legacy/anthropic}/__init__.py +0 -0
  269. /vanna/{azuresearch → legacy/azuresearch}/__init__.py +0 -0
  270. /vanna/{base → legacy/base}/__init__.py +0 -0
  271. /vanna/{chromadb → legacy/chromadb}/__init__.py +0 -0
  272. /vanna/{deepseek → legacy/deepseek}/__init__.py +0 -0
  273. /vanna/{exceptions → legacy/exceptions}/__init__.py +0 -0
  274. /vanna/{google → legacy/google}/__init__.py +0 -0
  275. /vanna/{hf → legacy/hf}/__init__.py +0 -0
  276. /vanna/{local.py → legacy/local.py} +0 -0
  277. /vanna/{marqo → legacy/marqo}/__init__.py +0 -0
  278. /vanna/{marqo → legacy/marqo}/marqo.py +0 -0
  279. /vanna/{milvus → legacy/milvus}/__init__.py +0 -0
  280. /vanna/{mistral → legacy/mistral}/__init__.py +0 -0
  281. /vanna/{mistral → legacy/mistral}/mistral.py +0 -0
  282. /vanna/{mock → legacy/mock}/__init__.py +0 -0
  283. /vanna/{mock → legacy/mock}/embedding.py +0 -0
  284. /vanna/{ollama → legacy/ollama}/__init__.py +0 -0
  285. /vanna/{openai → legacy/openai}/__init__.py +0 -0
  286. /vanna/{openai → legacy/openai}/openai_embeddings.py +0 -0
  287. /vanna/{opensearch → legacy/opensearch}/__init__.py +0 -0
  288. /vanna/{oracle → legacy/oracle}/__init__.py +0 -0
  289. /vanna/{pgvector → legacy/pgvector}/__init__.py +0 -0
  290. /vanna/{pinecone → legacy/pinecone}/__init__.py +0 -0
  291. /vanna/{pinecone → legacy/pinecone}/pinecone_vector.py +0 -0
  292. /vanna/{qdrant → legacy/qdrant}/__init__.py +0 -0
  293. /vanna/{qianfan → legacy/qianfan}/__init__.py +0 -0
  294. /vanna/{qianwen → legacy/qianwen}/QianwenAI_embeddings.py +0 -0
  295. /vanna/{qianwen → legacy/qianwen}/__init__.py +0 -0
  296. /vanna/{types → legacy/types}/__init__.py +0 -0
  297. /vanna/{vannadb → legacy/vannadb}/__init__.py +0 -0
  298. /vanna/{vllm → legacy/vllm}/__init__.py +0 -0
  299. /vanna/{weaviate → legacy/weaviate}/__init__.py +0 -0
  300. /vanna/{xinference → legacy/xinference}/__init__.py +0 -0
  301. {vanna-0.7.9.dist-info → vanna-2.0.0.dist-info}/WHEEL +0 -0
  302. {vanna-0.7.9.dist-info → vanna-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,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
+ )