vanna 0.7.9__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vanna/__init__.py +167 -395
- vanna/agents/__init__.py +7 -0
- vanna/capabilities/__init__.py +17 -0
- vanna/capabilities/agent_memory/__init__.py +21 -0
- vanna/capabilities/agent_memory/base.py +103 -0
- vanna/capabilities/agent_memory/models.py +53 -0
- vanna/capabilities/file_system/__init__.py +14 -0
- vanna/capabilities/file_system/base.py +71 -0
- vanna/capabilities/file_system/models.py +25 -0
- vanna/capabilities/sql_runner/__init__.py +13 -0
- vanna/capabilities/sql_runner/base.py +37 -0
- vanna/capabilities/sql_runner/models.py +13 -0
- vanna/components/__init__.py +92 -0
- vanna/components/base.py +11 -0
- vanna/components/rich/__init__.py +83 -0
- vanna/components/rich/containers/__init__.py +7 -0
- vanna/components/rich/containers/card.py +20 -0
- vanna/components/rich/data/__init__.py +9 -0
- vanna/components/rich/data/chart.py +17 -0
- vanna/components/rich/data/dataframe.py +93 -0
- vanna/components/rich/feedback/__init__.py +21 -0
- vanna/components/rich/feedback/badge.py +16 -0
- vanna/components/rich/feedback/icon_text.py +14 -0
- vanna/components/rich/feedback/log_viewer.py +41 -0
- vanna/components/rich/feedback/notification.py +19 -0
- vanna/components/rich/feedback/progress.py +37 -0
- vanna/components/rich/feedback/status_card.py +28 -0
- vanna/components/rich/feedback/status_indicator.py +14 -0
- vanna/components/rich/interactive/__init__.py +21 -0
- vanna/components/rich/interactive/button.py +95 -0
- vanna/components/rich/interactive/task_list.py +58 -0
- vanna/components/rich/interactive/ui_state.py +93 -0
- vanna/components/rich/specialized/__init__.py +7 -0
- vanna/components/rich/specialized/artifact.py +20 -0
- vanna/components/rich/text.py +16 -0
- vanna/components/simple/__init__.py +15 -0
- vanna/components/simple/image.py +15 -0
- vanna/components/simple/link.py +15 -0
- vanna/components/simple/text.py +11 -0
- vanna/core/__init__.py +193 -0
- vanna/core/_compat.py +19 -0
- vanna/core/agent/__init__.py +10 -0
- vanna/core/agent/agent.py +1407 -0
- vanna/core/agent/config.py +123 -0
- vanna/core/audit/__init__.py +28 -0
- vanna/core/audit/base.py +299 -0
- vanna/core/audit/models.py +131 -0
- vanna/core/component_manager.py +329 -0
- vanna/core/components.py +53 -0
- vanna/core/enhancer/__init__.py +11 -0
- vanna/core/enhancer/base.py +94 -0
- vanna/core/enhancer/default.py +118 -0
- vanna/core/enricher/__init__.py +10 -0
- vanna/core/enricher/base.py +59 -0
- vanna/core/errors.py +47 -0
- vanna/core/evaluation/__init__.py +81 -0
- vanna/core/evaluation/base.py +186 -0
- vanna/core/evaluation/dataset.py +254 -0
- vanna/core/evaluation/evaluators.py +376 -0
- vanna/core/evaluation/report.py +289 -0
- vanna/core/evaluation/runner.py +313 -0
- vanna/core/filter/__init__.py +10 -0
- vanna/core/filter/base.py +67 -0
- vanna/core/lifecycle/__init__.py +10 -0
- vanna/core/lifecycle/base.py +83 -0
- vanna/core/llm/__init__.py +16 -0
- vanna/core/llm/base.py +40 -0
- vanna/core/llm/models.py +61 -0
- vanna/core/middleware/__init__.py +10 -0
- vanna/core/middleware/base.py +69 -0
- vanna/core/observability/__init__.py +11 -0
- vanna/core/observability/base.py +88 -0
- vanna/core/observability/models.py +47 -0
- vanna/core/recovery/__init__.py +11 -0
- vanna/core/recovery/base.py +84 -0
- vanna/core/recovery/models.py +32 -0
- vanna/core/registry.py +278 -0
- vanna/core/rich_component.py +156 -0
- vanna/core/simple_component.py +27 -0
- vanna/core/storage/__init__.py +14 -0
- vanna/core/storage/base.py +46 -0
- vanna/core/storage/models.py +46 -0
- vanna/core/system_prompt/__init__.py +13 -0
- vanna/core/system_prompt/base.py +36 -0
- vanna/core/system_prompt/default.py +157 -0
- vanna/core/tool/__init__.py +18 -0
- vanna/core/tool/base.py +70 -0
- vanna/core/tool/models.py +84 -0
- vanna/core/user/__init__.py +17 -0
- vanna/core/user/base.py +29 -0
- vanna/core/user/models.py +25 -0
- vanna/core/user/request_context.py +70 -0
- vanna/core/user/resolver.py +42 -0
- vanna/core/validation.py +164 -0
- vanna/core/workflow/__init__.py +12 -0
- vanna/core/workflow/base.py +254 -0
- vanna/core/workflow/default.py +789 -0
- vanna/examples/__init__.py +1 -0
- vanna/examples/__main__.py +44 -0
- vanna/examples/anthropic_quickstart.py +80 -0
- vanna/examples/artifact_example.py +293 -0
- vanna/examples/claude_sqlite_example.py +236 -0
- vanna/examples/coding_agent_example.py +300 -0
- vanna/examples/custom_system_prompt_example.py +174 -0
- vanna/examples/default_workflow_handler_example.py +208 -0
- vanna/examples/email_auth_example.py +340 -0
- vanna/examples/evaluation_example.py +269 -0
- vanna/examples/extensibility_example.py +262 -0
- vanna/examples/minimal_example.py +67 -0
- vanna/examples/mock_auth_example.py +227 -0
- vanna/examples/mock_custom_tool.py +311 -0
- vanna/examples/mock_quickstart.py +79 -0
- vanna/examples/mock_quota_example.py +145 -0
- vanna/examples/mock_rich_components_demo.py +396 -0
- vanna/examples/mock_sqlite_example.py +223 -0
- vanna/examples/openai_quickstart.py +83 -0
- vanna/examples/primitive_components_demo.py +305 -0
- vanna/examples/quota_lifecycle_example.py +139 -0
- vanna/examples/visualization_example.py +251 -0
- vanna/integrations/__init__.py +17 -0
- vanna/integrations/anthropic/__init__.py +9 -0
- vanna/integrations/anthropic/llm.py +270 -0
- vanna/integrations/azureopenai/__init__.py +9 -0
- vanna/integrations/azureopenai/llm.py +329 -0
- vanna/integrations/azuresearch/__init__.py +7 -0
- vanna/integrations/azuresearch/agent_memory.py +413 -0
- vanna/integrations/bigquery/__init__.py +5 -0
- vanna/integrations/bigquery/sql_runner.py +81 -0
- vanna/integrations/chromadb/__init__.py +104 -0
- vanna/integrations/chromadb/agent_memory.py +416 -0
- vanna/integrations/clickhouse/__init__.py +5 -0
- vanna/integrations/clickhouse/sql_runner.py +82 -0
- vanna/integrations/duckdb/__init__.py +5 -0
- vanna/integrations/duckdb/sql_runner.py +65 -0
- vanna/integrations/faiss/__init__.py +7 -0
- vanna/integrations/faiss/agent_memory.py +431 -0
- vanna/integrations/google/__init__.py +9 -0
- vanna/integrations/google/gemini.py +370 -0
- vanna/integrations/hive/__init__.py +5 -0
- vanna/integrations/hive/sql_runner.py +87 -0
- vanna/integrations/local/__init__.py +17 -0
- vanna/integrations/local/agent_memory/__init__.py +7 -0
- vanna/integrations/local/agent_memory/in_memory.py +285 -0
- vanna/integrations/local/audit.py +59 -0
- vanna/integrations/local/file_system.py +242 -0
- vanna/integrations/local/file_system_conversation_store.py +255 -0
- vanna/integrations/local/storage.py +62 -0
- vanna/integrations/marqo/__init__.py +7 -0
- vanna/integrations/marqo/agent_memory.py +354 -0
- vanna/integrations/milvus/__init__.py +7 -0
- vanna/integrations/milvus/agent_memory.py +458 -0
- vanna/integrations/mock/__init__.py +9 -0
- vanna/integrations/mock/llm.py +65 -0
- vanna/integrations/mssql/__init__.py +5 -0
- vanna/integrations/mssql/sql_runner.py +66 -0
- vanna/integrations/mysql/__init__.py +5 -0
- vanna/integrations/mysql/sql_runner.py +92 -0
- vanna/integrations/ollama/__init__.py +7 -0
- vanna/integrations/ollama/llm.py +252 -0
- vanna/integrations/openai/__init__.py +10 -0
- vanna/integrations/openai/llm.py +267 -0
- vanna/integrations/openai/responses.py +163 -0
- vanna/integrations/opensearch/__init__.py +7 -0
- vanna/integrations/opensearch/agent_memory.py +411 -0
- vanna/integrations/oracle/__init__.py +5 -0
- vanna/integrations/oracle/sql_runner.py +75 -0
- vanna/integrations/pinecone/__init__.py +7 -0
- vanna/integrations/pinecone/agent_memory.py +329 -0
- vanna/integrations/plotly/__init__.py +5 -0
- vanna/integrations/plotly/chart_generator.py +313 -0
- vanna/integrations/postgres/__init__.py +9 -0
- vanna/integrations/postgres/sql_runner.py +112 -0
- vanna/integrations/premium/agent_memory/__init__.py +7 -0
- vanna/integrations/premium/agent_memory/premium.py +186 -0
- vanna/integrations/presto/__init__.py +5 -0
- vanna/integrations/presto/sql_runner.py +107 -0
- vanna/integrations/qdrant/__init__.py +7 -0
- vanna/integrations/qdrant/agent_memory.py +461 -0
- vanna/integrations/snowflake/__init__.py +5 -0
- vanna/integrations/snowflake/sql_runner.py +147 -0
- vanna/integrations/sqlite/__init__.py +9 -0
- vanna/integrations/sqlite/sql_runner.py +65 -0
- vanna/integrations/weaviate/__init__.py +7 -0
- vanna/integrations/weaviate/agent_memory.py +428 -0
- vanna/{ZhipuAI → legacy/ZhipuAI}/ZhipuAI_embeddings.py +11 -11
- vanna/legacy/__init__.py +403 -0
- vanna/legacy/adapter.py +463 -0
- vanna/{advanced → legacy/advanced}/__init__.py +3 -1
- vanna/{anthropic → legacy/anthropic}/anthropic_chat.py +9 -7
- vanna/{azuresearch → legacy/azuresearch}/azuresearch_vector.py +79 -41
- vanna/{base → legacy/base}/base.py +224 -217
- vanna/legacy/bedrock/__init__.py +1 -0
- vanna/{bedrock → legacy/bedrock}/bedrock_converse.py +13 -12
- vanna/{chromadb → legacy/chromadb}/chromadb_vector.py +3 -1
- vanna/legacy/cohere/__init__.py +2 -0
- vanna/{cohere → legacy/cohere}/cohere_chat.py +19 -14
- vanna/{cohere → legacy/cohere}/cohere_embeddings.py +25 -19
- vanna/{deepseek → legacy/deepseek}/deepseek_chat.py +5 -6
- vanna/legacy/faiss/__init__.py +1 -0
- vanna/{faiss → legacy/faiss}/faiss.py +113 -59
- vanna/{flask → legacy/flask}/__init__.py +84 -43
- vanna/{flask → legacy/flask}/assets.py +5 -5
- vanna/{flask → legacy/flask}/auth.py +5 -4
- vanna/{google → legacy/google}/bigquery_vector.py +75 -42
- vanna/{google → legacy/google}/gemini_chat.py +7 -3
- vanna/{hf → legacy/hf}/hf.py +0 -1
- vanna/{milvus → legacy/milvus}/milvus_vector.py +58 -35
- vanna/{mock → legacy/mock}/llm.py +0 -1
- vanna/legacy/mock/vectordb.py +67 -0
- vanna/legacy/ollama/ollama.py +110 -0
- vanna/{openai → legacy/openai}/openai_chat.py +2 -6
- vanna/legacy/opensearch/opensearch_vector.py +369 -0
- vanna/legacy/opensearch/opensearch_vector_semantic.py +200 -0
- vanna/legacy/oracle/oracle_vector.py +584 -0
- vanna/{pgvector → legacy/pgvector}/pgvector.py +42 -13
- vanna/{qdrant → legacy/qdrant}/qdrant.py +2 -6
- vanna/legacy/qianfan/Qianfan_Chat.py +170 -0
- vanna/legacy/qianfan/Qianfan_embeddings.py +36 -0
- vanna/legacy/qianwen/QianwenAI_chat.py +132 -0
- vanna/{remote.py → legacy/remote.py} +28 -26
- vanna/{utils.py → legacy/utils.py} +6 -11
- vanna/{vannadb → legacy/vannadb}/vannadb_vector.py +115 -46
- vanna/{vllm → legacy/vllm}/vllm.py +5 -6
- vanna/{weaviate → legacy/weaviate}/weaviate_vector.py +59 -40
- vanna/{xinference → legacy/xinference}/xinference.py +6 -6
- vanna/py.typed +0 -0
- vanna/servers/__init__.py +16 -0
- vanna/servers/__main__.py +8 -0
- vanna/servers/base/__init__.py +18 -0
- vanna/servers/base/chat_handler.py +65 -0
- vanna/servers/base/models.py +111 -0
- vanna/servers/base/rich_chat_handler.py +141 -0
- vanna/servers/base/templates.py +331 -0
- vanna/servers/cli/__init__.py +7 -0
- vanna/servers/cli/server_runner.py +204 -0
- vanna/servers/fastapi/__init__.py +7 -0
- vanna/servers/fastapi/app.py +163 -0
- vanna/servers/fastapi/routes.py +183 -0
- vanna/servers/flask/__init__.py +7 -0
- vanna/servers/flask/app.py +132 -0
- vanna/servers/flask/routes.py +137 -0
- vanna/tools/__init__.py +41 -0
- vanna/tools/agent_memory.py +322 -0
- vanna/tools/file_system.py +879 -0
- vanna/tools/python.py +222 -0
- vanna/tools/run_sql.py +165 -0
- vanna/tools/visualize_data.py +195 -0
- vanna/utils/__init__.py +0 -0
- vanna/web_components/__init__.py +44 -0
- vanna-2.0.0.dist-info/METADATA +485 -0
- vanna-2.0.0.dist-info/RECORD +289 -0
- vanna-2.0.0.dist-info/entry_points.txt +3 -0
- vanna/bedrock/__init__.py +0 -1
- vanna/cohere/__init__.py +0 -2
- vanna/faiss/__init__.py +0 -1
- vanna/mock/vectordb.py +0 -55
- vanna/ollama/ollama.py +0 -103
- vanna/opensearch/opensearch_vector.py +0 -392
- vanna/opensearch/opensearch_vector_semantic.py +0 -175
- vanna/oracle/oracle_vector.py +0 -585
- vanna/qianfan/Qianfan_Chat.py +0 -165
- vanna/qianfan/Qianfan_embeddings.py +0 -36
- vanna/qianwen/QianwenAI_chat.py +0 -133
- vanna-0.7.9.dist-info/METADATA +0 -408
- vanna-0.7.9.dist-info/RECORD +0 -79
- /vanna/{ZhipuAI → legacy/ZhipuAI}/ZhipuAI_Chat.py +0 -0
- /vanna/{ZhipuAI → legacy/ZhipuAI}/__init__.py +0 -0
- /vanna/{anthropic → legacy/anthropic}/__init__.py +0 -0
- /vanna/{azuresearch → legacy/azuresearch}/__init__.py +0 -0
- /vanna/{base → legacy/base}/__init__.py +0 -0
- /vanna/{chromadb → legacy/chromadb}/__init__.py +0 -0
- /vanna/{deepseek → legacy/deepseek}/__init__.py +0 -0
- /vanna/{exceptions → legacy/exceptions}/__init__.py +0 -0
- /vanna/{google → legacy/google}/__init__.py +0 -0
- /vanna/{hf → legacy/hf}/__init__.py +0 -0
- /vanna/{local.py → legacy/local.py} +0 -0
- /vanna/{marqo → legacy/marqo}/__init__.py +0 -0
- /vanna/{marqo → legacy/marqo}/marqo.py +0 -0
- /vanna/{milvus → legacy/milvus}/__init__.py +0 -0
- /vanna/{mistral → legacy/mistral}/__init__.py +0 -0
- /vanna/{mistral → legacy/mistral}/mistral.py +0 -0
- /vanna/{mock → legacy/mock}/__init__.py +0 -0
- /vanna/{mock → legacy/mock}/embedding.py +0 -0
- /vanna/{ollama → legacy/ollama}/__init__.py +0 -0
- /vanna/{openai → legacy/openai}/__init__.py +0 -0
- /vanna/{openai → legacy/openai}/openai_embeddings.py +0 -0
- /vanna/{opensearch → legacy/opensearch}/__init__.py +0 -0
- /vanna/{oracle → legacy/oracle}/__init__.py +0 -0
- /vanna/{pgvector → legacy/pgvector}/__init__.py +0 -0
- /vanna/{pinecone → legacy/pinecone}/__init__.py +0 -0
- /vanna/{pinecone → legacy/pinecone}/pinecone_vector.py +0 -0
- /vanna/{qdrant → legacy/qdrant}/__init__.py +0 -0
- /vanna/{qianfan → legacy/qianfan}/__init__.py +0 -0
- /vanna/{qianwen → legacy/qianwen}/QianwenAI_embeddings.py +0 -0
- /vanna/{qianwen → legacy/qianwen}/__init__.py +0 -0
- /vanna/{types → legacy/types}/__init__.py +0 -0
- /vanna/{vannadb → legacy/vannadb}/__init__.py +0 -0
- /vanna/{vllm → legacy/vllm}/__init__.py +0 -0
- /vanna/{weaviate → legacy/weaviate}/__init__.py +0 -0
- /vanna/{xinference → legacy/xinference}/__init__.py +0 -0
- {vanna-0.7.9.dist-info → vanna-2.0.0.dist-info}/WHEEL +0 -0
- {vanna-0.7.9.dist-info → vanna-2.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File system conversation store implementation.
|
|
3
|
+
|
|
4
|
+
This module provides a file-based implementation of the ConversationStore
|
|
5
|
+
interface that persists conversations to disk as a directory structure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
from vanna.core.storage import ConversationStore, Conversation, Message
|
|
16
|
+
from vanna.core.user import User
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FileSystemConversationStore(ConversationStore):
|
|
20
|
+
"""File system-based conversation store.
|
|
21
|
+
|
|
22
|
+
Stores conversations as directories with individual message files:
|
|
23
|
+
conversations/{conversation_id}/
|
|
24
|
+
metadata.json - conversation metadata (id, user info, timestamps)
|
|
25
|
+
messages/
|
|
26
|
+
{timestamp}_{index}.json - individual message files
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, base_dir: str = "conversations") -> None:
|
|
30
|
+
"""Initialize the file system conversation store.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
base_dir: Base directory for storing conversations
|
|
34
|
+
"""
|
|
35
|
+
self.base_dir = Path(base_dir)
|
|
36
|
+
self.base_dir.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
def _get_conversation_dir(self, conversation_id: str) -> Path:
|
|
39
|
+
"""Get the directory path for a conversation."""
|
|
40
|
+
return self.base_dir / conversation_id
|
|
41
|
+
|
|
42
|
+
def _get_metadata_path(self, conversation_id: str) -> Path:
|
|
43
|
+
"""Get the metadata file path for a conversation."""
|
|
44
|
+
return self._get_conversation_dir(conversation_id) / "metadata.json"
|
|
45
|
+
|
|
46
|
+
def _get_messages_dir(self, conversation_id: str) -> Path:
|
|
47
|
+
"""Get the messages directory for a conversation."""
|
|
48
|
+
return self._get_conversation_dir(conversation_id) / "messages"
|
|
49
|
+
|
|
50
|
+
def _save_metadata(self, conversation: Conversation) -> None:
|
|
51
|
+
"""Save conversation metadata to disk."""
|
|
52
|
+
conv_dir = self._get_conversation_dir(conversation.id)
|
|
53
|
+
conv_dir.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
|
|
55
|
+
metadata = {
|
|
56
|
+
"id": conversation.id,
|
|
57
|
+
"user": conversation.user.model_dump(mode="json"),
|
|
58
|
+
"created_at": conversation.created_at.isoformat(),
|
|
59
|
+
"updated_at": conversation.updated_at.isoformat(),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
metadata_path = self._get_metadata_path(conversation.id)
|
|
63
|
+
with open(metadata_path, "w") as f:
|
|
64
|
+
json.dump(metadata, f, indent=2)
|
|
65
|
+
|
|
66
|
+
def _load_messages(self, conversation_id: str) -> List[Message]:
|
|
67
|
+
"""Load all messages for a conversation."""
|
|
68
|
+
messages_dir = self._get_messages_dir(conversation_id)
|
|
69
|
+
|
|
70
|
+
if not messages_dir.exists():
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
messages = []
|
|
74
|
+
# Sort message files by name (timestamp_index ensures correct order)
|
|
75
|
+
message_files = sorted(messages_dir.glob("*.json"))
|
|
76
|
+
|
|
77
|
+
for file_path in message_files:
|
|
78
|
+
try:
|
|
79
|
+
with open(file_path, "r") as f:
|
|
80
|
+
data = json.load(f)
|
|
81
|
+
message = Message.model_validate(data)
|
|
82
|
+
messages.append(message)
|
|
83
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
84
|
+
print(f"Failed to load message from {file_path}: {e}")
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
return messages
|
|
88
|
+
|
|
89
|
+
def _append_message(
|
|
90
|
+
self, conversation_id: str, message: Message, index: int
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Append a message to the conversation."""
|
|
93
|
+
messages_dir = self._get_messages_dir(conversation_id)
|
|
94
|
+
messages_dir.mkdir(parents=True, exist_ok=True)
|
|
95
|
+
|
|
96
|
+
# Use timestamp + index to ensure unique, ordered filenames
|
|
97
|
+
timestamp = int(time.time() * 1000000) # microseconds
|
|
98
|
+
filename = f"{timestamp}_{index:06d}.json"
|
|
99
|
+
file_path = messages_dir / filename
|
|
100
|
+
|
|
101
|
+
with open(file_path, "w") as f:
|
|
102
|
+
json.dump(message.model_dump(mode="json"), f, indent=2)
|
|
103
|
+
|
|
104
|
+
async def create_conversation(
|
|
105
|
+
self, conversation_id: str, user: User, initial_message: str
|
|
106
|
+
) -> Conversation:
|
|
107
|
+
"""Create a new conversation with the specified ID."""
|
|
108
|
+
conversation = Conversation(
|
|
109
|
+
id=conversation_id,
|
|
110
|
+
user=user,
|
|
111
|
+
messages=[Message(role="user", content=initial_message)],
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Save metadata
|
|
115
|
+
self._save_metadata(conversation)
|
|
116
|
+
|
|
117
|
+
# Save initial message
|
|
118
|
+
self._append_message(conversation_id, conversation.messages[0], 0)
|
|
119
|
+
|
|
120
|
+
return conversation
|
|
121
|
+
|
|
122
|
+
async def get_conversation(
|
|
123
|
+
self, conversation_id: str, user: User
|
|
124
|
+
) -> Optional[Conversation]:
|
|
125
|
+
"""Get conversation by ID, scoped to user."""
|
|
126
|
+
metadata_path = self._get_metadata_path(conversation_id)
|
|
127
|
+
|
|
128
|
+
if not metadata_path.exists():
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
# Load metadata
|
|
133
|
+
with open(metadata_path, "r") as f:
|
|
134
|
+
metadata = json.load(f)
|
|
135
|
+
|
|
136
|
+
# Verify ownership
|
|
137
|
+
if metadata["user"]["id"] != user.id:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
# Load all messages
|
|
141
|
+
messages = self._load_messages(conversation_id)
|
|
142
|
+
|
|
143
|
+
# Reconstruct conversation
|
|
144
|
+
conversation = Conversation(
|
|
145
|
+
id=metadata["id"],
|
|
146
|
+
user=User.model_validate(metadata["user"]),
|
|
147
|
+
messages=messages,
|
|
148
|
+
created_at=datetime.fromisoformat(metadata["created_at"]),
|
|
149
|
+
updated_at=datetime.fromisoformat(metadata["updated_at"]),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return conversation
|
|
153
|
+
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
|
154
|
+
print(f"Failed to load conversation {conversation_id}: {e}")
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
async def update_conversation(self, conversation: Conversation) -> None:
|
|
158
|
+
"""Update conversation with new messages."""
|
|
159
|
+
# Update the updated_at timestamp
|
|
160
|
+
conversation.updated_at = datetime.now()
|
|
161
|
+
|
|
162
|
+
# Save updated metadata
|
|
163
|
+
self._save_metadata(conversation)
|
|
164
|
+
|
|
165
|
+
# Get existing messages count to determine new message indices
|
|
166
|
+
existing_messages = self._load_messages(conversation.id)
|
|
167
|
+
existing_count = len(existing_messages)
|
|
168
|
+
|
|
169
|
+
# Only append new messages (ones not already saved)
|
|
170
|
+
for i, message in enumerate(
|
|
171
|
+
conversation.messages[existing_count:], start=existing_count
|
|
172
|
+
):
|
|
173
|
+
self._append_message(conversation.id, message, i)
|
|
174
|
+
|
|
175
|
+
async def delete_conversation(self, conversation_id: str, user: User) -> bool:
|
|
176
|
+
"""Delete conversation."""
|
|
177
|
+
conv_dir = self._get_conversation_dir(conversation_id)
|
|
178
|
+
|
|
179
|
+
if not conv_dir.exists():
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
# Verify ownership before deleting
|
|
183
|
+
conversation = await self.get_conversation(conversation_id, user)
|
|
184
|
+
if not conversation:
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
# Delete all message files
|
|
189
|
+
messages_dir = self._get_messages_dir(conversation_id)
|
|
190
|
+
if messages_dir.exists():
|
|
191
|
+
for file_path in messages_dir.glob("*.json"):
|
|
192
|
+
file_path.unlink()
|
|
193
|
+
messages_dir.rmdir()
|
|
194
|
+
|
|
195
|
+
# Delete metadata
|
|
196
|
+
metadata_path = self._get_metadata_path(conversation_id)
|
|
197
|
+
if metadata_path.exists():
|
|
198
|
+
metadata_path.unlink()
|
|
199
|
+
|
|
200
|
+
# Delete conversation directory
|
|
201
|
+
conv_dir.rmdir()
|
|
202
|
+
|
|
203
|
+
return True
|
|
204
|
+
except OSError as e:
|
|
205
|
+
print(f"Failed to delete conversation {conversation_id}: {e}")
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
async def list_conversations(
|
|
209
|
+
self, user: User, limit: int = 50, offset: int = 0
|
|
210
|
+
) -> List[Conversation]:
|
|
211
|
+
"""List conversations for user."""
|
|
212
|
+
if not self.base_dir.exists():
|
|
213
|
+
return []
|
|
214
|
+
|
|
215
|
+
conversations = []
|
|
216
|
+
|
|
217
|
+
# Iterate through all conversation directories
|
|
218
|
+
for conv_dir in self.base_dir.iterdir():
|
|
219
|
+
if not conv_dir.is_dir():
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
metadata_path = conv_dir / "metadata.json"
|
|
223
|
+
if not metadata_path.exists():
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
# Load metadata
|
|
228
|
+
with open(metadata_path, "r") as f:
|
|
229
|
+
metadata = json.load(f)
|
|
230
|
+
|
|
231
|
+
# Skip conversations not owned by this user
|
|
232
|
+
if metadata["user"]["id"] != user.id:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# Load messages
|
|
236
|
+
messages = self._load_messages(conv_dir.name)
|
|
237
|
+
|
|
238
|
+
# Reconstruct conversation
|
|
239
|
+
conversation = Conversation(
|
|
240
|
+
id=metadata["id"],
|
|
241
|
+
user=User.model_validate(metadata["user"]),
|
|
242
|
+
messages=messages,
|
|
243
|
+
created_at=datetime.fromisoformat(metadata["created_at"]),
|
|
244
|
+
updated_at=datetime.fromisoformat(metadata["updated_at"]),
|
|
245
|
+
)
|
|
246
|
+
conversations.append(conversation)
|
|
247
|
+
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
|
248
|
+
print(f"Failed to load conversation from {conv_dir}: {e}")
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
# Sort by updated_at desc
|
|
252
|
+
conversations.sort(key=lambda x: x.updated_at, reverse=True)
|
|
253
|
+
|
|
254
|
+
# Apply pagination
|
|
255
|
+
return conversations[offset : offset + limit]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
In-memory conversation store implementation.
|
|
3
|
+
|
|
4
|
+
This module provides a simple in-memory implementation of the ConversationStore
|
|
5
|
+
interface, useful for testing and development.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from vanna.core.storage import ConversationStore, Conversation, Message
|
|
11
|
+
from vanna.core.user import User
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MemoryConversationStore(ConversationStore):
|
|
15
|
+
"""In-memory conversation store."""
|
|
16
|
+
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._conversations: Dict[str, Conversation] = {}
|
|
19
|
+
|
|
20
|
+
async def create_conversation(
|
|
21
|
+
self, conversation_id: str, user: User, initial_message: str
|
|
22
|
+
) -> Conversation:
|
|
23
|
+
"""Create a new conversation with the specified ID."""
|
|
24
|
+
conversation = Conversation(
|
|
25
|
+
id=conversation_id,
|
|
26
|
+
user=user,
|
|
27
|
+
messages=[Message(role="user", content=initial_message)],
|
|
28
|
+
)
|
|
29
|
+
self._conversations[conversation_id] = conversation
|
|
30
|
+
return conversation
|
|
31
|
+
|
|
32
|
+
async def get_conversation(
|
|
33
|
+
self, conversation_id: str, user: User
|
|
34
|
+
) -> Optional[Conversation]:
|
|
35
|
+
"""Get conversation by ID, scoped to user."""
|
|
36
|
+
conversation = self._conversations.get(conversation_id)
|
|
37
|
+
if conversation and conversation.user.id == user.id:
|
|
38
|
+
return conversation
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
async def update_conversation(self, conversation: Conversation) -> None:
|
|
42
|
+
"""Update conversation with new messages."""
|
|
43
|
+
self._conversations[conversation.id] = conversation
|
|
44
|
+
|
|
45
|
+
async def delete_conversation(self, conversation_id: str, user: User) -> bool:
|
|
46
|
+
"""Delete conversation."""
|
|
47
|
+
conversation = await self.get_conversation(conversation_id, user)
|
|
48
|
+
if conversation:
|
|
49
|
+
del self._conversations[conversation_id]
|
|
50
|
+
return True
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
async def list_conversations(
|
|
54
|
+
self, user: User, limit: int = 50, offset: int = 0
|
|
55
|
+
) -> List[Conversation]:
|
|
56
|
+
"""List conversations for user."""
|
|
57
|
+
user_conversations = [
|
|
58
|
+
conv for conv in self._conversations.values() if conv.user.id == user.id
|
|
59
|
+
]
|
|
60
|
+
# Sort by updated_at desc
|
|
61
|
+
user_conversations.sort(key=lambda x: x.updated_at, reverse=True)
|
|
62
|
+
return user_conversations[offset : offset + limit]
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Marqo vector database implementation of AgentMemory.
|
|
3
|
+
|
|
4
|
+
This implementation uses Marqo for 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
|
+
import marqo
|
|
16
|
+
|
|
17
|
+
MARQO_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
MARQO_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 MarqoAgentMemory(AgentMemory):
|
|
32
|
+
"""Marqo-based implementation of AgentMemory."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
url: str = "http://localhost:8882",
|
|
37
|
+
index_name: str = "tool-memories",
|
|
38
|
+
api_key: Optional[str] = None,
|
|
39
|
+
):
|
|
40
|
+
if not MARQO_AVAILABLE:
|
|
41
|
+
raise ImportError(
|
|
42
|
+
"Marqo is required for MarqoAgentMemory. Install with: pip install marqo"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self.url = url
|
|
46
|
+
self.index_name = index_name
|
|
47
|
+
self.api_key = api_key
|
|
48
|
+
self._client = None
|
|
49
|
+
self._executor = ThreadPoolExecutor(max_workers=2)
|
|
50
|
+
|
|
51
|
+
def _get_client(self):
|
|
52
|
+
"""Get or create Marqo client."""
|
|
53
|
+
if self._client is None:
|
|
54
|
+
self._client = marqo.Client(url=self.url, api_key=self.api_key)
|
|
55
|
+
|
|
56
|
+
# Create index if it doesn't exist
|
|
57
|
+
try:
|
|
58
|
+
self._client.get_index(self.index_name)
|
|
59
|
+
except Exception:
|
|
60
|
+
self._client.create_index(self.index_name)
|
|
61
|
+
|
|
62
|
+
return self._client
|
|
63
|
+
|
|
64
|
+
async def save_tool_usage(
|
|
65
|
+
self,
|
|
66
|
+
question: str,
|
|
67
|
+
tool_name: str,
|
|
68
|
+
args: Dict[str, Any],
|
|
69
|
+
context: ToolContext,
|
|
70
|
+
success: bool = True,
|
|
71
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Save a tool usage pattern."""
|
|
74
|
+
|
|
75
|
+
def _save():
|
|
76
|
+
client = self._get_client()
|
|
77
|
+
|
|
78
|
+
memory_id = str(uuid.uuid4())
|
|
79
|
+
timestamp = datetime.now().isoformat()
|
|
80
|
+
|
|
81
|
+
document = {
|
|
82
|
+
"_id": memory_id,
|
|
83
|
+
"question": question,
|
|
84
|
+
"tool_name": tool_name,
|
|
85
|
+
"args": json.dumps(args),
|
|
86
|
+
"timestamp": timestamp,
|
|
87
|
+
"success": success,
|
|
88
|
+
"metadata": json.dumps(metadata or {}),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
client.index(self.index_name).add_documents(
|
|
92
|
+
[document], tensor_fields=["question"]
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
await asyncio.get_event_loop().run_in_executor(self._executor, _save)
|
|
96
|
+
|
|
97
|
+
async def search_similar_usage(
|
|
98
|
+
self,
|
|
99
|
+
question: str,
|
|
100
|
+
context: ToolContext,
|
|
101
|
+
*,
|
|
102
|
+
limit: int = 10,
|
|
103
|
+
similarity_threshold: float = 0.7,
|
|
104
|
+
tool_name_filter: Optional[str] = None,
|
|
105
|
+
) -> List[ToolMemorySearchResult]:
|
|
106
|
+
"""Search for similar tool usage patterns."""
|
|
107
|
+
|
|
108
|
+
def _search():
|
|
109
|
+
client = self._get_client()
|
|
110
|
+
|
|
111
|
+
# Build filter
|
|
112
|
+
filter_string = "success:true"
|
|
113
|
+
if tool_name_filter:
|
|
114
|
+
filter_string += f" AND tool_name:{tool_name_filter}"
|
|
115
|
+
|
|
116
|
+
results = client.index(self.index_name).search(
|
|
117
|
+
q=question, limit=limit, filter_string=filter_string
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
search_results = []
|
|
121
|
+
for i, hit in enumerate(results["hits"]):
|
|
122
|
+
# Marqo returns score
|
|
123
|
+
similarity_score = hit.get("_score", 0)
|
|
124
|
+
|
|
125
|
+
if similarity_score >= similarity_threshold:
|
|
126
|
+
args = json.loads(hit.get("args", "{}"))
|
|
127
|
+
metadata_dict = json.loads(hit.get("metadata", "{}"))
|
|
128
|
+
|
|
129
|
+
memory = ToolMemory(
|
|
130
|
+
memory_id=hit["_id"],
|
|
131
|
+
question=hit["question"],
|
|
132
|
+
tool_name=hit["tool_name"],
|
|
133
|
+
args=args,
|
|
134
|
+
timestamp=hit.get("timestamp"),
|
|
135
|
+
success=hit.get("success", True),
|
|
136
|
+
metadata=metadata_dict,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
search_results.append(
|
|
140
|
+
ToolMemorySearchResult(
|
|
141
|
+
memory=memory, similarity_score=similarity_score, rank=i + 1
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return search_results
|
|
146
|
+
|
|
147
|
+
return await asyncio.get_event_loop().run_in_executor(self._executor, _search)
|
|
148
|
+
|
|
149
|
+
async def get_recent_memories(
|
|
150
|
+
self, context: ToolContext, limit: int = 10
|
|
151
|
+
) -> List[ToolMemory]:
|
|
152
|
+
"""Get recently added memories."""
|
|
153
|
+
|
|
154
|
+
def _get_recent():
|
|
155
|
+
client = self._get_client()
|
|
156
|
+
|
|
157
|
+
# Search with wildcard and sort by timestamp
|
|
158
|
+
results = client.index(self.index_name).search(
|
|
159
|
+
q="*", limit=limit, sort="timestamp:desc"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
memories = []
|
|
163
|
+
for hit in results.get("hits", []):
|
|
164
|
+
args = json.loads(hit.get("args", "{}"))
|
|
165
|
+
metadata_dict = json.loads(hit.get("metadata", "{}"))
|
|
166
|
+
|
|
167
|
+
memory = ToolMemory(
|
|
168
|
+
memory_id=hit["_id"],
|
|
169
|
+
question=hit["question"],
|
|
170
|
+
tool_name=hit["tool_name"],
|
|
171
|
+
args=args,
|
|
172
|
+
timestamp=hit.get("timestamp"),
|
|
173
|
+
success=hit.get("success", True),
|
|
174
|
+
metadata=metadata_dict,
|
|
175
|
+
)
|
|
176
|
+
memories.append(memory)
|
|
177
|
+
|
|
178
|
+
return memories
|
|
179
|
+
|
|
180
|
+
return await asyncio.get_event_loop().run_in_executor(
|
|
181
|
+
self._executor, _get_recent
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
async def delete_by_id(self, context: ToolContext, memory_id: str) -> bool:
|
|
185
|
+
"""Delete a memory by its ID."""
|
|
186
|
+
|
|
187
|
+
def _delete():
|
|
188
|
+
client = self._get_client()
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
client.index(self.index_name).delete_documents(ids=[memory_id])
|
|
192
|
+
return True
|
|
193
|
+
except Exception:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)
|
|
197
|
+
|
|
198
|
+
async def save_text_memory(self, content: str, context: ToolContext) -> TextMemory:
|
|
199
|
+
"""Save a text memory."""
|
|
200
|
+
|
|
201
|
+
def _save():
|
|
202
|
+
client = self._get_client()
|
|
203
|
+
|
|
204
|
+
memory_id = str(uuid.uuid4())
|
|
205
|
+
timestamp = datetime.now().isoformat()
|
|
206
|
+
|
|
207
|
+
document = {
|
|
208
|
+
"_id": memory_id,
|
|
209
|
+
"content": content,
|
|
210
|
+
"timestamp": timestamp,
|
|
211
|
+
"is_text_memory": True,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
client.index(self.index_name).add_documents(
|
|
215
|
+
[document], tensor_fields=["content"]
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return TextMemory(memory_id=memory_id, content=content, timestamp=timestamp)
|
|
219
|
+
|
|
220
|
+
return await asyncio.get_event_loop().run_in_executor(self._executor, _save)
|
|
221
|
+
|
|
222
|
+
async def search_text_memories(
|
|
223
|
+
self,
|
|
224
|
+
query: str,
|
|
225
|
+
context: ToolContext,
|
|
226
|
+
*,
|
|
227
|
+
limit: int = 10,
|
|
228
|
+
similarity_threshold: float = 0.7,
|
|
229
|
+
) -> List[TextMemorySearchResult]:
|
|
230
|
+
"""Search for similar text memories."""
|
|
231
|
+
|
|
232
|
+
def _search():
|
|
233
|
+
client = self._get_client()
|
|
234
|
+
|
|
235
|
+
filter_string = "is_text_memory:true"
|
|
236
|
+
|
|
237
|
+
results = client.index(self.index_name).search(
|
|
238
|
+
q=query, limit=limit, filter_string=filter_string
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
search_results = []
|
|
242
|
+
for i, hit in enumerate(results["hits"]):
|
|
243
|
+
similarity_score = hit.get("_score", 0)
|
|
244
|
+
|
|
245
|
+
if similarity_score >= similarity_threshold:
|
|
246
|
+
memory = TextMemory(
|
|
247
|
+
memory_id=hit["_id"],
|
|
248
|
+
content=hit.get("content", ""),
|
|
249
|
+
timestamp=hit.get("timestamp"),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
search_results.append(
|
|
253
|
+
TextMemorySearchResult(
|
|
254
|
+
memory=memory, similarity_score=similarity_score, rank=i + 1
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return search_results
|
|
259
|
+
|
|
260
|
+
return await asyncio.get_event_loop().run_in_executor(self._executor, _search)
|
|
261
|
+
|
|
262
|
+
async def get_recent_text_memories(
|
|
263
|
+
self, context: ToolContext, limit: int = 10
|
|
264
|
+
) -> List[TextMemory]:
|
|
265
|
+
"""Get recently added text memories."""
|
|
266
|
+
|
|
267
|
+
def _get_recent():
|
|
268
|
+
client = self._get_client()
|
|
269
|
+
|
|
270
|
+
results = client.index(self.index_name).search(
|
|
271
|
+
q="*",
|
|
272
|
+
limit=limit,
|
|
273
|
+
filter_string="is_text_memory:true",
|
|
274
|
+
sort="timestamp:desc",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
memories = []
|
|
278
|
+
for hit in results.get("hits", []):
|
|
279
|
+
memory = TextMemory(
|
|
280
|
+
memory_id=hit["_id"],
|
|
281
|
+
content=hit.get("content", ""),
|
|
282
|
+
timestamp=hit.get("timestamp"),
|
|
283
|
+
)
|
|
284
|
+
memories.append(memory)
|
|
285
|
+
|
|
286
|
+
return memories
|
|
287
|
+
|
|
288
|
+
return await asyncio.get_event_loop().run_in_executor(
|
|
289
|
+
self._executor, _get_recent
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
async def delete_text_memory(self, context: ToolContext, memory_id: str) -> bool:
|
|
293
|
+
"""Delete a text memory by its ID."""
|
|
294
|
+
|
|
295
|
+
def _delete():
|
|
296
|
+
client = self._get_client()
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
client.index(self.index_name).delete_documents(ids=[memory_id])
|
|
300
|
+
return True
|
|
301
|
+
except Exception:
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
return await asyncio.get_event_loop().run_in_executor(self._executor, _delete)
|
|
305
|
+
|
|
306
|
+
async def clear_memories(
|
|
307
|
+
self,
|
|
308
|
+
context: ToolContext,
|
|
309
|
+
tool_name: Optional[str] = None,
|
|
310
|
+
before_date: Optional[str] = None,
|
|
311
|
+
) -> int:
|
|
312
|
+
"""Clear stored memories."""
|
|
313
|
+
|
|
314
|
+
def _clear():
|
|
315
|
+
client = self._get_client()
|
|
316
|
+
|
|
317
|
+
# Build filter for search
|
|
318
|
+
filter_parts = []
|
|
319
|
+
if tool_name:
|
|
320
|
+
filter_parts.append(f"tool_name:{tool_name}")
|
|
321
|
+
if before_date:
|
|
322
|
+
filter_parts.append(f"timestamp:[* TO {before_date}]")
|
|
323
|
+
|
|
324
|
+
if filter_parts or (tool_name is None and before_date is None):
|
|
325
|
+
filter_string = " AND ".join(filter_parts) if filter_parts else None
|
|
326
|
+
|
|
327
|
+
if filter_string:
|
|
328
|
+
# Search for documents to delete
|
|
329
|
+
results = client.index(self.index_name).search(
|
|
330
|
+
q="*",
|
|
331
|
+
limit=1000, # Max results
|
|
332
|
+
filter_string=filter_string,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
ids_to_delete = [hit["_id"] for hit in results.get("hits", [])]
|
|
336
|
+
|
|
337
|
+
if ids_to_delete:
|
|
338
|
+
client.index(self.index_name).delete_documents(
|
|
339
|
+
ids=ids_to_delete
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return len(ids_to_delete)
|
|
343
|
+
else:
|
|
344
|
+
# Delete entire index and recreate
|
|
345
|
+
try:
|
|
346
|
+
client.delete_index(self.index_name)
|
|
347
|
+
client.create_index(self.index_name)
|
|
348
|
+
except Exception:
|
|
349
|
+
pass
|
|
350
|
+
return 0
|
|
351
|
+
|
|
352
|
+
return 0
|
|
353
|
+
|
|
354
|
+
return await asyncio.get_event_loop().run_in_executor(self._executor, _clear)
|