solana-agent 1.4.4__tar.gz → 2.0.1__tar.gz
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.
- {solana_agent-1.4.4 → solana_agent-2.0.1}/PKG-INFO +6 -8
- {solana_agent-1.4.4 → solana_agent-2.0.1}/README.md +4 -5
- {solana_agent-1.4.4 → solana_agent-2.0.1}/pyproject.toml +2 -3
- {solana_agent-1.4.4 → solana_agent-2.0.1}/solana_agent/ai.py +333 -490
- {solana_agent-1.4.4 → solana_agent-2.0.1}/LICENSE +0 -0
- {solana_agent-1.4.4 → solana_agent-2.0.1}/solana_agent/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: solana-agent
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: Build self-learning AI Agents
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: ai,openai,ai agents
|
|
@@ -16,8 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
17
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
18
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
-
Requires-Dist:
|
|
20
|
-
Requires-Dist: openai (>=1.63.2,<2.0.0)
|
|
19
|
+
Requires-Dist: openai (>=1.64.0,<2.0.0)
|
|
21
20
|
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
|
22
21
|
Requires-Dist: pinecone (>=6.0.1,<7.0.0)
|
|
23
22
|
Requires-Dist: pydantic (>=2.10.6,<3.0.0)
|
|
@@ -58,16 +57,15 @@ Unlike traditional AI assistants that forget conversations after each session, S
|
|
|
58
57
|
- Real-time voice-to-voice conversations
|
|
59
58
|
|
|
60
59
|
🧠 **Memory System and Extensibility**
|
|
61
|
-
- Advanced AI memory combining conversational context,
|
|
60
|
+
- Advanced AI memory combining conversational context, knowledge base, and parallel tool calling
|
|
62
61
|
- Create custom tools for extending the Agent's capabilities like further API integrations
|
|
63
62
|
|
|
64
63
|
🔍 **Multi-Source Search and Reasoning**
|
|
65
64
|
- Internet search via Perplexity
|
|
66
65
|
- X (Twitter) search using Grok
|
|
67
|
-
- Conversational
|
|
66
|
+
- Conversational memory powered by Zep
|
|
68
67
|
- Conversational message history using MongoDB (on-prem or hosted)
|
|
69
|
-
- Knowledge Base (KB) using Pinecone with reranking
|
|
70
|
-
- File uploading and searching using OpenAI like for PDFs
|
|
68
|
+
- Knowledge Base (KB) using Pinecone with reranking - available globally or user-specific
|
|
71
69
|
- Upload CSVs to be processed into summary reports and stored in the Knowledge Base (KB) using Gemini
|
|
72
70
|
- Comprehensive reasoning combining multiple data sources
|
|
73
71
|
|
|
@@ -80,7 +78,7 @@ Unlike traditional AI assistants that forget conversations after each session, S
|
|
|
80
78
|
- Persistent cross-session knowledge retention
|
|
81
79
|
- Automatic self-learning from conversations
|
|
82
80
|
- Knowledge Base to add domain specific knowledge
|
|
83
|
-
-
|
|
81
|
+
- CSV file uploads to perform document context search
|
|
84
82
|
|
|
85
83
|
🏢 **Enterprise Focus**
|
|
86
84
|
- Production-ready out of the box in a few lines of code
|
|
@@ -29,16 +29,15 @@ Unlike traditional AI assistants that forget conversations after each session, S
|
|
|
29
29
|
- Real-time voice-to-voice conversations
|
|
30
30
|
|
|
31
31
|
🧠 **Memory System and Extensibility**
|
|
32
|
-
- Advanced AI memory combining conversational context,
|
|
32
|
+
- Advanced AI memory combining conversational context, knowledge base, and parallel tool calling
|
|
33
33
|
- Create custom tools for extending the Agent's capabilities like further API integrations
|
|
34
34
|
|
|
35
35
|
🔍 **Multi-Source Search and Reasoning**
|
|
36
36
|
- Internet search via Perplexity
|
|
37
37
|
- X (Twitter) search using Grok
|
|
38
|
-
- Conversational
|
|
38
|
+
- Conversational memory powered by Zep
|
|
39
39
|
- Conversational message history using MongoDB (on-prem or hosted)
|
|
40
|
-
- Knowledge Base (KB) using Pinecone with reranking
|
|
41
|
-
- File uploading and searching using OpenAI like for PDFs
|
|
40
|
+
- Knowledge Base (KB) using Pinecone with reranking - available globally or user-specific
|
|
42
41
|
- Upload CSVs to be processed into summary reports and stored in the Knowledge Base (KB) using Gemini
|
|
43
42
|
- Comprehensive reasoning combining multiple data sources
|
|
44
43
|
|
|
@@ -51,7 +50,7 @@ Unlike traditional AI assistants that forget conversations after each session, S
|
|
|
51
50
|
- Persistent cross-session knowledge retention
|
|
52
51
|
- Automatic self-learning from conversations
|
|
53
52
|
- Knowledge Base to add domain specific knowledge
|
|
54
|
-
-
|
|
53
|
+
- CSV file uploads to perform document context search
|
|
55
54
|
|
|
56
55
|
🏢 **Enterprise Focus**
|
|
57
56
|
- Production-ready out of the box in a few lines of code
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "solana-agent"
|
|
3
|
-
version = "
|
|
3
|
+
version = "2.0.1"
|
|
4
4
|
description = "Build self-learning AI Agents"
|
|
5
5
|
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -18,13 +18,12 @@ python_paths = [".", "tests"]
|
|
|
18
18
|
|
|
19
19
|
[tool.poetry.dependencies]
|
|
20
20
|
python = ">=3.9,<4.0"
|
|
21
|
-
openai = "^1.
|
|
21
|
+
openai = "^1.64.0"
|
|
22
22
|
pydantic = "^2.10.6"
|
|
23
23
|
pymongo = "^4.11.1"
|
|
24
24
|
zep-cloud = "^2.4.0"
|
|
25
25
|
requests = "^2.32.3"
|
|
26
26
|
pinecone = "^6.0.1"
|
|
27
|
-
cohere = "^5.13.12"
|
|
28
27
|
pandas = "^2.2.3"
|
|
29
28
|
|
|
30
29
|
[build-system]
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import datetime
|
|
3
3
|
import json
|
|
4
|
-
from typing import AsyncGenerator,
|
|
4
|
+
from typing import AsyncGenerator, List, Literal, Dict, Any, Callable
|
|
5
5
|
import uuid
|
|
6
|
-
import cohere
|
|
7
6
|
import pandas as pd
|
|
8
7
|
from pydantic import BaseModel
|
|
9
8
|
from pymongo import MongoClient
|
|
10
9
|
from openai import OpenAI
|
|
11
|
-
from openai import AssistantEventHandler
|
|
12
|
-
from openai.types.beta.threads import TextDelta, Text
|
|
13
|
-
from typing_extensions import override
|
|
14
10
|
import inspect
|
|
15
11
|
import requests
|
|
16
12
|
from zep_cloud.client import AsyncZep
|
|
@@ -19,88 +15,46 @@ from zep_cloud.types import Message
|
|
|
19
15
|
from pinecone import Pinecone
|
|
20
16
|
|
|
21
17
|
|
|
22
|
-
class
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
self._tool_handlers = tool_handlers
|
|
26
|
-
self._ai_instance = ai_instance
|
|
27
|
-
|
|
28
|
-
@override
|
|
29
|
-
def on_text_delta(self, delta: TextDelta, snapshot: Text):
|
|
30
|
-
asyncio.create_task(
|
|
31
|
-
self._ai_instance._accumulated_value_queue.put(delta.value))
|
|
32
|
-
|
|
33
|
-
@override
|
|
34
|
-
def on_event(self, event):
|
|
35
|
-
if event.event == "thread.run.requires_action":
|
|
36
|
-
run_id = event.data.id
|
|
37
|
-
self._ai_instance._handle_requires_action(event.data, run_id)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class ToolConfig(BaseModel):
|
|
41
|
-
name: str
|
|
42
|
-
description: str
|
|
43
|
-
parameters: Dict[str, Any]
|
|
18
|
+
class DocumentModel(BaseModel):
|
|
19
|
+
id: str
|
|
20
|
+
text: str
|
|
44
21
|
|
|
45
22
|
|
|
46
23
|
class MongoDatabase:
|
|
47
24
|
def __init__(self, db_url: str, db_name: str):
|
|
48
25
|
self._client = MongoClient(db_url)
|
|
49
26
|
self.db = self._client[db_name]
|
|
50
|
-
self._threads = self.db["threads"]
|
|
51
27
|
self.messages = self.db["messages"]
|
|
52
28
|
self.kb = self.db["kb"]
|
|
53
|
-
self.vector_stores = self.db["vector_stores"]
|
|
54
|
-
self.files = self.db["files"]
|
|
55
|
-
|
|
56
|
-
def save_thread_id(self, user_id: str, thread_id: str):
|
|
57
|
-
self._threads.insert_one({"thread_id": thread_id, "user_id": user_id})
|
|
58
|
-
|
|
59
|
-
def get_thread_id(self, user_id: str) -> Optional[str]:
|
|
60
|
-
document = self._threads.find_one({"user_id": user_id})
|
|
61
|
-
return document["thread_id"] if document else None
|
|
62
29
|
|
|
63
30
|
def save_message(self, user_id: str, metadata: Dict[str, Any]):
|
|
64
31
|
metadata["user_id"] = user_id
|
|
65
32
|
self.messages.insert_one(metadata)
|
|
66
33
|
|
|
67
|
-
def delete_all_threads(self):
|
|
68
|
-
self._threads.delete_many({})
|
|
69
|
-
|
|
70
34
|
def clear_user_history(self, user_id: str):
|
|
71
35
|
self.messages.delete_many({"user_id": user_id})
|
|
72
|
-
self._threads.delete_one({"user_id": user_id})
|
|
73
|
-
|
|
74
|
-
def add_document_to_kb(self, id: str, namespace: str, document: str):
|
|
75
|
-
storage = {}
|
|
76
|
-
storage["namespace"] = namespace
|
|
77
|
-
storage["reference"] = id
|
|
78
|
-
storage["document"] = document
|
|
79
|
-
storage["timestamp"] = datetime.datetime.now(datetime.timezone.utc)
|
|
80
|
-
self.kb.insert_one(storage)
|
|
81
|
-
|
|
82
|
-
def get_vector_store_id(self) -> str | None:
|
|
83
|
-
document = self.vector_stores.find_one()
|
|
84
|
-
return document["vector_store_id"] if document else None
|
|
85
36
|
|
|
86
|
-
def
|
|
87
|
-
|
|
37
|
+
def add_documents_to_kb(self, namespace: str, documents: List[DocumentModel]):
|
|
38
|
+
for document in documents:
|
|
39
|
+
storage = {}
|
|
40
|
+
storage["namespace"] = namespace
|
|
41
|
+
storage["reference"] = document.id
|
|
42
|
+
storage["document"] = document.text
|
|
43
|
+
storage["timestamp"] = datetime.datetime.now(datetime.timezone.utc)
|
|
44
|
+
self.kb.insert_one(storage)
|
|
88
45
|
|
|
89
|
-
def
|
|
90
|
-
self.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def delete_file(self, file_id: str):
|
|
96
|
-
self.files.delete_one({"file_id": file_id})
|
|
46
|
+
def list_documents_in_kb(self, namespace: str) -> List[DocumentModel]:
|
|
47
|
+
documents = self.kb.find({"namespace": namespace})
|
|
48
|
+
return [
|
|
49
|
+
DocumentModel(id=doc["reference"], text=doc["document"])
|
|
50
|
+
for doc in documents
|
|
51
|
+
]
|
|
97
52
|
|
|
98
53
|
|
|
99
54
|
class AI:
|
|
100
55
|
def __init__(
|
|
101
56
|
self,
|
|
102
57
|
openai_api_key: str,
|
|
103
|
-
name: str,
|
|
104
58
|
instructions: str,
|
|
105
59
|
database: Any,
|
|
106
60
|
zep_api_key: str = None,
|
|
@@ -108,74 +62,55 @@ class AI:
|
|
|
108
62
|
grok_api_key: str = None,
|
|
109
63
|
pinecone_api_key: str = None,
|
|
110
64
|
pinecone_index_name: str = None,
|
|
111
|
-
|
|
112
|
-
cohere_model: Literal["rerank-v3.5"] = "rerank-v3.5",
|
|
65
|
+
pinecone_embed_model: Literal["llama-text-embed-v2"] = "llama-text-embed-v2",
|
|
113
66
|
gemini_api_key: str = None,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
openai_assistant_model: Literal["gpt-4o-mini",
|
|
117
|
-
"gpt-4o"] = "gpt-4o-mini",
|
|
118
|
-
openai_embedding_model: Literal[
|
|
119
|
-
"text-embedding-3-small", "text-embedding-3-large"
|
|
120
|
-
] = "text-embedding-3-large",
|
|
67
|
+
openai_base_url: str = None,
|
|
68
|
+
openai_model: str = "gpt-4o-mini",
|
|
121
69
|
):
|
|
122
|
-
"""Initialize a new AI assistant
|
|
70
|
+
"""Initialize a new AI assistant instance.
|
|
123
71
|
|
|
124
72
|
Args:
|
|
125
73
|
openai_api_key (str): OpenAI API key for core AI functionality
|
|
126
|
-
name (str): Name identifier for the assistant
|
|
127
74
|
instructions (str): Base behavioral instructions for the AI
|
|
128
75
|
database (Any): Database instance for message/thread storage
|
|
129
|
-
zep_api_key (str, optional): API key for Zep memory
|
|
76
|
+
zep_api_key (str, optional): API key for Zep memory storage. Defaults to None
|
|
130
77
|
perplexity_api_key (str, optional): API key for Perplexity search. Defaults to None
|
|
131
78
|
grok_api_key (str, optional): API key for X/Twitter search via Grok. Defaults to None
|
|
132
79
|
pinecone_api_key (str, optional): API key for Pinecone. Defaults to None
|
|
133
80
|
pinecone_index_name (str, optional): Name of the Pinecone index. Defaults to None
|
|
134
|
-
|
|
135
|
-
cohere_model (Literal["rerank-v3.5"], optional): Cohere model for reranking. Defaults to "rerank-v3.5"
|
|
81
|
+
pinecone_embed_model (Literal["llama-text-embed-v2"], optional): Pinecone embedding model. Defaults to "llama-text-embed-v2"
|
|
136
82
|
gemini_api_key (str, optional): API key for Gemini search. Defaults to None
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
openai_assistant_model (Literal["gpt-4o-mini", "gpt-4o"], optional): OpenAI model for assistant. Defaults to "gpt-4o-mini"
|
|
140
|
-
openai_embedding_model (Literal["text-embedding-3-small", "text-embedding-3-large"], optional): OpenAI model for text embedding. Defaults to "text-embedding-3-large"
|
|
83
|
+
openai_base_url (str, optional): Base URL for OpenAI API. Defaults to None
|
|
84
|
+
openai_model (str, optional): OpenAI model to use. Defaults to "gpt-4o-mini"
|
|
141
85
|
|
|
142
86
|
Example:
|
|
143
87
|
```python
|
|
144
88
|
ai = AI(
|
|
145
89
|
openai_api_key="your-key",
|
|
146
|
-
name="Assistant",
|
|
147
90
|
instructions="Be helpful and concise",
|
|
148
|
-
database=MongoDatabase("mongodb://localhost", "ai_db")
|
|
91
|
+
database=MongoDatabase("mongodb://localhost", "ai_db")
|
|
149
92
|
)
|
|
150
93
|
```
|
|
151
94
|
Notes:
|
|
152
95
|
- Requires valid OpenAI API key for core functionality
|
|
96
|
+
- Supports any OpenAI compatible model for conversation
|
|
97
|
+
- Requires valid Zep API key for memory storage
|
|
153
98
|
- Database instance for storing messages and threads
|
|
154
|
-
- Optional integrations for
|
|
155
|
-
- Supports code interpretation and custom tool functions
|
|
99
|
+
- Optional integrations for Perplexity, Pinecone, Gemini, and Grok
|
|
156
100
|
- You must create the Pinecone index in the dashboard before using it
|
|
157
101
|
"""
|
|
158
|
-
self._client = OpenAI(api_key=openai_api_key)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if code_interpreter
|
|
171
|
-
else [{"type": "file_search"}]
|
|
172
|
-
)
|
|
173
|
-
else:
|
|
174
|
-
self._tools = [{"type": "code_interpreter"}
|
|
175
|
-
] if code_interpreter else []
|
|
176
|
-
|
|
177
|
-
self._tool_handlers = {}
|
|
178
|
-
self._assistant_id = None
|
|
102
|
+
self._client = OpenAI(api_key=openai_api_key, base_url=openai_base_url) if openai_base_url else OpenAI(
|
|
103
|
+
api_key=openai_api_key)
|
|
104
|
+
memory_instructions = """
|
|
105
|
+
You are a highly intelligent, context-aware conversational AI. When a user sends a query or statement, you should not only process the current input but also retrieve and integrate relevant context from their previous interactions. Use the memory data to:
|
|
106
|
+
- Infer nuances in the user's intent.
|
|
107
|
+
- Recall previous topics, preferences, or facts that might be relevant.
|
|
108
|
+
- Provide a thoughtful, clear, and structured response.
|
|
109
|
+
- Clarify ambiguous queries by relating them to known user history.
|
|
110
|
+
|
|
111
|
+
Always be concise and ensure that your response maintains coherence across the conversation while respecting the user's context and previous data.
|
|
112
|
+
"""
|
|
113
|
+
self._instructions = instructions + " " + memory_instructions
|
|
179
114
|
self._database: MongoDatabase = database
|
|
180
115
|
self._accumulated_value_queue = asyncio.Queue()
|
|
181
116
|
self._zep = AsyncZep(api_key=zep_api_key) if zep_api_key else None
|
|
@@ -187,94 +122,22 @@ class AI:
|
|
|
187
122
|
Pinecone(api_key=pinecone_api_key) if pinecone_api_key else None
|
|
188
123
|
)
|
|
189
124
|
self._pinecone_index_name = pinecone_index_name if pinecone_index_name else None
|
|
125
|
+
self._pinecone_embedding_model = pinecone_embed_model
|
|
190
126
|
self.kb = (
|
|
191
127
|
self._pinecone.Index(
|
|
192
128
|
self._pinecone_index_name) if self._pinecone else None
|
|
193
129
|
)
|
|
194
|
-
self.
|
|
195
|
-
|
|
196
|
-
self.
|
|
130
|
+
self._openai_base_url = openai_base_url
|
|
131
|
+
self._openai_model = openai_model
|
|
132
|
+
self._tools = []
|
|
197
133
|
|
|
198
134
|
async def __aenter__(self):
|
|
199
|
-
assistants = self._client.beta.assistants.list()
|
|
200
|
-
existing_assistant = next(
|
|
201
|
-
(a for a in assistants if a.name == self._name), None)
|
|
202
|
-
|
|
203
|
-
if existing_assistant:
|
|
204
|
-
self._assistant_id = existing_assistant.id
|
|
205
|
-
else:
|
|
206
|
-
self._assistant_id = self._client.beta.assistants.create(
|
|
207
|
-
name=self._name,
|
|
208
|
-
instructions=self._instructions,
|
|
209
|
-
tools=self._tools,
|
|
210
|
-
model=self._openai_assistant_model,
|
|
211
|
-
).id
|
|
212
|
-
self._database.delete_all_threads()
|
|
213
|
-
if self._file_search:
|
|
214
|
-
vectore_store_id = self._database.get_vector_store_id()
|
|
215
|
-
if vectore_store_id:
|
|
216
|
-
self._vector_store = self._client.beta.vector_stores.retrieve(
|
|
217
|
-
vector_store_id=vectore_store_id
|
|
218
|
-
)
|
|
219
|
-
else:
|
|
220
|
-
uid = uuid.uuid4().hex
|
|
221
|
-
self._vector_store = self._client.beta.vector_stores.create(
|
|
222
|
-
name=uid)
|
|
223
|
-
self._database.save_vector_store_id(self._vector_store.id)
|
|
224
|
-
self._client.beta.assistants.update(
|
|
225
|
-
assistant_id=self._assistant_id,
|
|
226
|
-
tool_resources={
|
|
227
|
-
"file_search": {"vector_store_ids": [self._vector_store.id]}
|
|
228
|
-
},
|
|
229
|
-
)
|
|
230
135
|
return self
|
|
231
136
|
|
|
232
137
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
233
138
|
# Perform any cleanup actions here
|
|
234
139
|
pass
|
|
235
140
|
|
|
236
|
-
async def _create_thread(self, user_id: str) -> str:
|
|
237
|
-
thread_id = self._database.get_thread_id(user_id)
|
|
238
|
-
|
|
239
|
-
if thread_id is None:
|
|
240
|
-
thread = self._client.beta.threads.create()
|
|
241
|
-
thread_id = thread.id
|
|
242
|
-
self._database.save_thread_id(user_id, thread_id)
|
|
243
|
-
if self._zep:
|
|
244
|
-
try:
|
|
245
|
-
await self._zep.user.add(user_id=user_id)
|
|
246
|
-
except Exception:
|
|
247
|
-
pass
|
|
248
|
-
try:
|
|
249
|
-
await self._zep.memory.add_session(
|
|
250
|
-
user_id=user_id, session_id=user_id
|
|
251
|
-
)
|
|
252
|
-
except Exception:
|
|
253
|
-
pass
|
|
254
|
-
|
|
255
|
-
return thread_id
|
|
256
|
-
|
|
257
|
-
async def _cancel_run(self, thread_id: str, run_id: str):
|
|
258
|
-
try:
|
|
259
|
-
self._client.beta.threads.runs.cancel(
|
|
260
|
-
thread_id=thread_id, run_id=run_id)
|
|
261
|
-
except Exception as e:
|
|
262
|
-
print(f"Error cancelling run: {e}")
|
|
263
|
-
|
|
264
|
-
async def _get_active_run(self, thread_id: str) -> Optional[str]:
|
|
265
|
-
runs = self._client.beta.threads.runs.list(
|
|
266
|
-
thread_id=thread_id, limit=1)
|
|
267
|
-
for run in runs:
|
|
268
|
-
if run.status in ["in_progress"]:
|
|
269
|
-
return run.id
|
|
270
|
-
return None
|
|
271
|
-
|
|
272
|
-
async def _get_run_status(self, thread_id: str, run_id: str) -> str:
|
|
273
|
-
run = self._client.beta.threads.runs.retrieve(
|
|
274
|
-
thread_id=thread_id, run_id=run_id
|
|
275
|
-
)
|
|
276
|
-
return run.status
|
|
277
|
-
|
|
278
141
|
def csv_to_text(self, file, filename: str) -> str:
|
|
279
142
|
"""Convert a CSV file to a Markdown table text format optimized for LLM ingestion.
|
|
280
143
|
|
|
@@ -368,20 +231,20 @@ class AI:
|
|
|
368
231
|
self,
|
|
369
232
|
file,
|
|
370
233
|
filename: str,
|
|
371
|
-
|
|
234
|
+
id: str = uuid.uuid4().hex,
|
|
235
|
+
prompt: str = "Summarize the table into a report, include important metrics and totals.",
|
|
372
236
|
namespace: str = "global",
|
|
373
|
-
model: Literal["gemini-2.0-flash",
|
|
374
|
-
"gemini-1.5-pro"] = "gemini-1.5-pro",
|
|
237
|
+
model: Literal["gemini-2.0-flash"] = "gemini-2.0-flash",
|
|
375
238
|
):
|
|
376
239
|
"""Upload and process a CSV file into the knowledge base with AI summarization.
|
|
377
240
|
|
|
378
241
|
Args:
|
|
379
242
|
file (BinaryIO): The CSV file to upload and process
|
|
380
243
|
filename (str): The name of the CSV file
|
|
381
|
-
|
|
244
|
+
id (str, optional): Unique identifier for the document. Defaults to a random UUID.
|
|
245
|
+
prompt (str, optional): Custom prompt for summarization. Defaults to "Summarize the table into a report, include important metrics and totals."
|
|
382
246
|
namespace (str, optional): Knowledge base namespace. Defaults to "global".
|
|
383
|
-
model (Literal["gemini-2.0-flash", "gemini-
|
|
384
|
-
Gemini model for summarization. Defaults to "gemini-1.5-pro"
|
|
247
|
+
model (Literal["gemini-2.0-flash"], optional): Gemini model for summarization. Defaults to "gemini-2.0-flash".
|
|
385
248
|
|
|
386
249
|
Example:
|
|
387
250
|
```python
|
|
@@ -393,123 +256,32 @@ class AI:
|
|
|
393
256
|
|
|
394
257
|
Note:
|
|
395
258
|
- Converts CSV to Markdown table format
|
|
396
|
-
- Uses Gemini AI to generate a summary
|
|
259
|
+
- Uses Gemini AI to generate a summary - total of 1M context tokens
|
|
397
260
|
- Stores summary in Pinecone knowledge base
|
|
398
261
|
- Requires configured Pinecone index
|
|
399
262
|
- Supports custom prompts for targeted summaries
|
|
400
263
|
"""
|
|
401
264
|
csv_text = self.csv_to_text(file, filename)
|
|
402
|
-
print(csv_text)
|
|
403
265
|
document = self.summarize(csv_text, prompt, model)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
def delete_vector_store_and_files(self):
|
|
408
|
-
"""Delete the OpenAI vector store and files.
|
|
409
|
-
|
|
410
|
-
Example:
|
|
411
|
-
```python
|
|
412
|
-
ai.delete_vector_store()
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
Note:
|
|
416
|
-
- Requires file_search=True in AI initialization
|
|
417
|
-
- Deletes the vector store and all associated files
|
|
418
|
-
"""
|
|
419
|
-
vector_store_id = self._database.get_vector_store_id()
|
|
420
|
-
if vector_store_id:
|
|
421
|
-
self._client.beta.vector_stores.delete(vector_store_id)
|
|
422
|
-
self._database.delete_vector_store_id(vector_store_id)
|
|
423
|
-
for file in self._database.files.find().to_list():
|
|
424
|
-
self._client.files.delete(file["file_id"])
|
|
425
|
-
self._database.delete_file(file["file_id"])
|
|
426
|
-
|
|
427
|
-
def max_files(self) -> bool:
|
|
428
|
-
"""Check if the OpenAI vector store has reached its maximum file capacity.
|
|
429
|
-
|
|
430
|
-
Returns:
|
|
431
|
-
bool: True if file count is at maximum (10,000), False otherwise
|
|
432
|
-
|
|
433
|
-
Example:
|
|
434
|
-
```python
|
|
435
|
-
if ai.max_files():
|
|
436
|
-
print("Vector store is full")
|
|
437
|
-
else:
|
|
438
|
-
print("Can still add more files")
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
Note:
|
|
442
|
-
- Requires file_search=True in AI initialization
|
|
443
|
-
- OpenAI vector stores have a 10,000 file limit
|
|
444
|
-
- Returns False if vector store is not configured
|
|
445
|
-
"""
|
|
446
|
-
self._vector_store.file_counts.completed == 10000
|
|
447
|
-
|
|
448
|
-
def file_count(self) -> int:
|
|
449
|
-
"""Get the total number of files processed in the OpenAI vector store.
|
|
450
|
-
|
|
451
|
-
Returns:
|
|
452
|
-
int: Number of successfully processed files in the vector store
|
|
453
|
-
|
|
454
|
-
Example:
|
|
455
|
-
```python
|
|
456
|
-
count = ai.file_count()
|
|
457
|
-
print(f"Processed {count} files")
|
|
458
|
-
# Returns: "Processed 5 files"
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
Note:
|
|
462
|
-
- Requires file_search=True in AI initialization
|
|
463
|
-
- Only counts successfully processed files
|
|
464
|
-
- Returns 0 if vector store is not configured
|
|
465
|
-
"""
|
|
466
|
-
self._vector_store.file_counts.completed
|
|
467
|
-
|
|
468
|
-
def add_file(
|
|
469
|
-
self,
|
|
470
|
-
filename: str,
|
|
471
|
-
file_stream: bytes,
|
|
472
|
-
) -> Literal["in_progress", "completed", "cancelled", "failed"]:
|
|
473
|
-
"""Upload and process a file in the OpenAI vector store.
|
|
474
|
-
|
|
475
|
-
Args:
|
|
476
|
-
filename (str): Name of the file to upload
|
|
477
|
-
file_stream (bytes): Raw bytes of the file to upload
|
|
478
|
-
|
|
479
|
-
Returns:
|
|
480
|
-
Literal["in_progress", "completed", "cancelled", "failed"]: Status of file processing
|
|
481
|
-
|
|
482
|
-
Example:
|
|
483
|
-
```python
|
|
484
|
-
with open('document.pdf', 'rb') as f:
|
|
485
|
-
status = ai.add_file(f.filename, f.read())
|
|
486
|
-
if status == "completed":
|
|
487
|
-
print("File processed successfully")
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
Note:
|
|
491
|
-
- Requires file_search=True in AI initialization
|
|
492
|
-
- Files are vectorized for semantic search
|
|
493
|
-
- Maximum file size: 512MB
|
|
494
|
-
- Maximum 10,000 files per vector store
|
|
495
|
-
- Processing may take a few seconds to minutes
|
|
496
|
-
"""
|
|
497
|
-
vector_store_id = self._database.get_vector_store_id()
|
|
498
|
-
file = self._client.files.create(
|
|
499
|
-
file=(filename, file_stream), purpose="assistants"
|
|
266
|
+
self.add_documents_to_kb(
|
|
267
|
+
documents=[DocumentModel(id=id, text=document)], namespace=namespace
|
|
500
268
|
)
|
|
501
|
-
file_batch = self._client.beta.vector_stores.files.create_and_poll(
|
|
502
|
-
vector_store_id=vector_store_id, file_id=file.id
|
|
503
|
-
)
|
|
504
|
-
self._database.add_file(file.id)
|
|
505
|
-
return file_batch.status
|
|
506
269
|
|
|
507
|
-
def search_kb(
|
|
508
|
-
|
|
270
|
+
def search_kb(
|
|
271
|
+
self,
|
|
272
|
+
query: str,
|
|
273
|
+
namespace: str = "global",
|
|
274
|
+
rerank_model: Literal["cohere-rerank-3.5"] = "cohere-rerank-3.5",
|
|
275
|
+
inner_limit: int = 10,
|
|
276
|
+
limit: int = 3,
|
|
277
|
+
) -> str:
|
|
278
|
+
"""Search Pinecone knowledge base.
|
|
509
279
|
|
|
510
280
|
Args:
|
|
511
281
|
query (str): Search query to find relevant documents
|
|
512
282
|
namespace (str, optional): Namespace of the Pinecone to search. Defaults to "global".
|
|
283
|
+
rerank_model (Literal["cohere-rerank-3.5"], optional): Rerank model on Pinecone. Defaults to "cohere-rerank-3.5".
|
|
284
|
+
inner_limit (int, optional): Maximum number of results to rerank. Defaults to 10.
|
|
513
285
|
limit (int, optional): Maximum number of results to return. Defaults to 3.
|
|
514
286
|
|
|
515
287
|
Returns:
|
|
@@ -517,25 +289,23 @@ class AI:
|
|
|
517
289
|
|
|
518
290
|
Example:
|
|
519
291
|
```python
|
|
520
|
-
results = ai.search_kb("
|
|
292
|
+
results = ai.search_kb("machine learning basics", "user123")
|
|
521
293
|
# Returns: '["Document 1", "Document 2", ...]'
|
|
522
294
|
```
|
|
523
295
|
|
|
524
296
|
Note:
|
|
525
297
|
- Requires configured Pinecone index
|
|
526
|
-
- Uses OpenAI embeddings for semantic search
|
|
527
|
-
- Returns JSON-serialized Pinecone match metadata results
|
|
528
298
|
- Returns error message string if search fails
|
|
529
|
-
- Optionally reranks results using Cohere API
|
|
530
299
|
"""
|
|
531
300
|
try:
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
301
|
+
embedding = self._pinecone.inference.embed(
|
|
302
|
+
model=self._pinecone_embedding_model,
|
|
303
|
+
inputs=[query],
|
|
304
|
+
parameters={"input_type": "query"},
|
|
535
305
|
)
|
|
536
306
|
search_results = self.kb.query(
|
|
537
|
-
vector=
|
|
538
|
-
top_k=
|
|
307
|
+
vector=embedding[0].values,
|
|
308
|
+
top_k=inner_limit,
|
|
539
309
|
include_metadata=False,
|
|
540
310
|
include_values=False,
|
|
541
311
|
namespace=namespace,
|
|
@@ -548,62 +318,88 @@ class AI:
|
|
|
548
318
|
for id in ids:
|
|
549
319
|
document = self._database.kb.find_one({"reference": id})
|
|
550
320
|
docs.append(document["document"])
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
new_docs
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
return json.dumps(new_docs)
|
|
564
|
-
except Exception:
|
|
565
|
-
return json.dumps(docs[:limit])
|
|
566
|
-
else:
|
|
321
|
+
try:
|
|
322
|
+
reranked_docs = self._pinecone.inference.rerank(
|
|
323
|
+
model=rerank_model,
|
|
324
|
+
query=query,
|
|
325
|
+
documents=docs,
|
|
326
|
+
top_n=limit,
|
|
327
|
+
)
|
|
328
|
+
new_docs = []
|
|
329
|
+
for doc in reranked_docs.data:
|
|
330
|
+
new_docs.append(docs[doc.index])
|
|
331
|
+
return json.dumps(new_docs)
|
|
332
|
+
except Exception:
|
|
567
333
|
return json.dumps(docs[:limit])
|
|
568
334
|
except Exception as e:
|
|
569
335
|
return f"Failed to search KB. Error: {e}"
|
|
570
336
|
|
|
571
|
-
def
|
|
337
|
+
def list_documents_in_kb(self, namespace: str = "global") -> List[DocumentModel]:
|
|
338
|
+
"""List all documents stored in the Pinecone knowledge base.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
namespace (str, optional): Namespace of the Pinecone index to search. Defaults to "global".
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
List[DocumentModel]: List of documents stored in the knowledge base
|
|
345
|
+
|
|
346
|
+
Example:
|
|
347
|
+
```python
|
|
348
|
+
documents = ai.list_documents_in_kb("user123")
|
|
349
|
+
for doc in documents:
|
|
350
|
+
print(doc)
|
|
351
|
+
# Returns: "Document 1", "Document 2", ...
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Note:
|
|
355
|
+
- Requires Pinecone index to be configured
|
|
356
|
+
"""
|
|
357
|
+
return self._database.list_documents_in_kb(namespace)
|
|
358
|
+
|
|
359
|
+
def add_documents_to_kb(
|
|
572
360
|
self,
|
|
573
|
-
|
|
574
|
-
id: str = uuid.uuid4().hex,
|
|
361
|
+
documents: List[DocumentModel],
|
|
575
362
|
namespace: str = "global",
|
|
576
363
|
):
|
|
577
|
-
"""Add
|
|
364
|
+
"""Add documents to the Pinecone knowledge base.
|
|
578
365
|
|
|
579
366
|
Args:
|
|
580
|
-
|
|
581
|
-
id (str, optional): Unique identifier for the document. Defaults to random UUID.
|
|
367
|
+
documents (List[DocumentModel]): List of documents to add to the knowledge base
|
|
582
368
|
namespace (str): Namespace of the Pinecone index to search. Defaults to "global".
|
|
583
369
|
|
|
584
370
|
Example:
|
|
585
371
|
```python
|
|
586
|
-
|
|
372
|
+
docs = [
|
|
373
|
+
{"id": "doc1", "text": "Document 1"},
|
|
374
|
+
{"id": "doc2", "text": "Document 2"},
|
|
375
|
+
]
|
|
376
|
+
ai.add_documents_to_kb(docs, "user123")
|
|
587
377
|
```
|
|
588
378
|
|
|
589
379
|
Note:
|
|
590
380
|
- Requires Pinecone index to be configured
|
|
591
|
-
- Uses OpenAI embeddings API
|
|
592
381
|
"""
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
382
|
+
embeddings = self._pinecone.inference.embed(
|
|
383
|
+
model=self._pinecone_embedding_model,
|
|
384
|
+
inputs=[d.text for d in documents],
|
|
385
|
+
parameters={"input_type": "passage", "truncate": "END"},
|
|
596
386
|
)
|
|
597
|
-
|
|
598
|
-
|
|
387
|
+
|
|
388
|
+
vectors = []
|
|
389
|
+
for d, e in zip(documents, embeddings):
|
|
390
|
+
vectors.append(
|
|
599
391
|
{
|
|
600
|
-
"id": id,
|
|
601
|
-
"values":
|
|
392
|
+
"id": d.id,
|
|
393
|
+
"values": e["values"],
|
|
602
394
|
}
|
|
603
|
-
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
self.kb.upsert(
|
|
398
|
+
vectors=vectors,
|
|
604
399
|
namespace=namespace,
|
|
605
400
|
)
|
|
606
|
-
|
|
401
|
+
|
|
402
|
+
self._database.add_documents_to_kb(namespace, documents)
|
|
607
403
|
|
|
608
404
|
def delete_document_from_kb(self, id: str, user_id: str = "global"):
|
|
609
405
|
"""Delete a document from the Pinecone knowledge base.
|
|
@@ -644,54 +440,37 @@ class AI:
|
|
|
644
440
|
"%Y-%m-%d %H:%M:%S %Z"
|
|
645
441
|
)
|
|
646
442
|
|
|
647
|
-
#
|
|
648
|
-
def
|
|
443
|
+
# has to be sync for tool
|
|
444
|
+
def get_memory_context(
|
|
649
445
|
self,
|
|
650
446
|
user_id: str,
|
|
651
|
-
query: str,
|
|
652
|
-
limit: int = 10,
|
|
653
447
|
) -> str:
|
|
654
|
-
"""
|
|
448
|
+
"""Retrieve contextual memory for a specific user from Zep memory storage.
|
|
655
449
|
|
|
656
450
|
Args:
|
|
657
|
-
user_id (str): Unique identifier for the user
|
|
658
|
-
query (str): Search query to find relevant facts
|
|
659
|
-
limit (int, optional): Maximum number of facts to return. Defaults to 10.
|
|
451
|
+
user_id (str): Unique identifier for the user whose memory context to retrieve
|
|
660
452
|
|
|
661
453
|
Returns:
|
|
662
|
-
str:
|
|
454
|
+
str: User's memory context or error message if retrieval fails
|
|
663
455
|
|
|
664
456
|
Example:
|
|
665
457
|
```python
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
)
|
|
670
|
-
# Returns: [{"fact": "user123 has 4 cats", "timestamp": "2022-01-01T12:00:00Z"}]
|
|
458
|
+
context = ai.get_memory_context("user123")
|
|
459
|
+
print(context)
|
|
460
|
+
# Returns: "User previously mentioned having 3 dogs and living in London"
|
|
671
461
|
```
|
|
672
462
|
|
|
673
463
|
Note:
|
|
674
|
-
|
|
675
|
-
|
|
464
|
+
- This is a synchronous tool method required for OpenAI function calling
|
|
465
|
+
- Requires Zep integration to be configured with valid API key
|
|
466
|
+
- Returns error message if Zep is not configured or retrieval fails
|
|
467
|
+
- Useful for maintaining conversation context across sessions
|
|
676
468
|
"""
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
session_ids=[user_id],
|
|
683
|
-
text=query,
|
|
684
|
-
limit=limit,
|
|
685
|
-
)
|
|
686
|
-
for result in results.results:
|
|
687
|
-
fact = result.fact.fact
|
|
688
|
-
timestamp = result.fact.created_at
|
|
689
|
-
facts.append({"fact": fact, "timestamp": timestamp})
|
|
690
|
-
return json.dumps(facts)
|
|
691
|
-
except Exception as e:
|
|
692
|
-
return f"Failed to search facts. Error: {e}"
|
|
693
|
-
else:
|
|
694
|
-
return "Zep integration not configured."
|
|
469
|
+
try:
|
|
470
|
+
memory = self._sync_zep.memory.get(session_id=user_id)
|
|
471
|
+
return memory.context
|
|
472
|
+
except Exception:
|
|
473
|
+
return ""
|
|
695
474
|
|
|
696
475
|
# search internet tool - has to be sync
|
|
697
476
|
def search_internet(
|
|
@@ -769,7 +548,6 @@ class AI:
|
|
|
769
548
|
prompt: str = "You combine the data with your reasoning to answer the query.",
|
|
770
549
|
use_perplexity: bool = True,
|
|
771
550
|
use_grok: bool = True,
|
|
772
|
-
use_facts: bool = True,
|
|
773
551
|
use_kb: bool = True,
|
|
774
552
|
perplexity_model: Literal[
|
|
775
553
|
"sonar", "sonar-pro", "sonar-reasoning-pro", "sonar-reasoning"
|
|
@@ -786,7 +564,6 @@ class AI:
|
|
|
786
564
|
prompt (str, optional): Prompt for reasoning. Defaults to "You combine the data with your reasoning to answer the query."
|
|
787
565
|
use_perplexity (bool, optional): Include Perplexity search results. Defaults to True
|
|
788
566
|
use_grok (bool, optional): Include X/Twitter search results. Defaults to True
|
|
789
|
-
use_facts (bool, optional): Include stored conversation facts. Defaults to True
|
|
790
567
|
use_kb (bool, optional): Include Pinecone knowledge base search results. Defaults to True
|
|
791
568
|
perplexity_model (Literal, optional): Perplexity model to use. Defaults to "sonar"
|
|
792
569
|
openai_model (Literal, optional): OpenAI model for reasoning. Defaults to "o3-mini"
|
|
@@ -818,13 +595,7 @@ class AI:
|
|
|
818
595
|
kb_results = ""
|
|
819
596
|
else:
|
|
820
597
|
kb_results = ""
|
|
821
|
-
|
|
822
|
-
try:
|
|
823
|
-
facts = self.search_facts(user_id, query)
|
|
824
|
-
except Exception:
|
|
825
|
-
facts = ""
|
|
826
|
-
else:
|
|
827
|
-
facts = ""
|
|
598
|
+
|
|
828
599
|
if use_perplexity:
|
|
829
600
|
try:
|
|
830
601
|
search_results = self.search_internet(
|
|
@@ -833,6 +604,7 @@ class AI:
|
|
|
833
604
|
search_results = ""
|
|
834
605
|
else:
|
|
835
606
|
search_results = ""
|
|
607
|
+
|
|
836
608
|
if use_grok:
|
|
837
609
|
try:
|
|
838
610
|
x_search_results = self.search_x(query, grok_model)
|
|
@@ -841,6 +613,11 @@ class AI:
|
|
|
841
613
|
else:
|
|
842
614
|
x_search_results = ""
|
|
843
615
|
|
|
616
|
+
if self._zep:
|
|
617
|
+
memory = self._sync_zep.memory.get(session_id=user_id)
|
|
618
|
+
else:
|
|
619
|
+
memory = ""
|
|
620
|
+
|
|
844
621
|
response = self._client.chat.completions.create(
|
|
845
622
|
model=openai_model,
|
|
846
623
|
messages=[
|
|
@@ -850,7 +627,7 @@ class AI:
|
|
|
850
627
|
},
|
|
851
628
|
{
|
|
852
629
|
"role": "user",
|
|
853
|
-
"content": f"Query: {query},
|
|
630
|
+
"content": f"Query: {query}, Memory: {memory}, KB Results: {kb_results}, Internet Search Results: {search_results}, X Search Results: {x_search_results}",
|
|
854
631
|
},
|
|
855
632
|
],
|
|
856
633
|
)
|
|
@@ -916,16 +693,12 @@ class AI:
|
|
|
916
693
|
Note:
|
|
917
694
|
This is an async method and must be awaited.
|
|
918
695
|
"""
|
|
919
|
-
try:
|
|
920
|
-
await self.delete_assistant_thread(user_id)
|
|
921
|
-
except Exception:
|
|
922
|
-
pass
|
|
923
696
|
try:
|
|
924
697
|
self._database.clear_user_history(user_id)
|
|
925
698
|
except Exception:
|
|
926
699
|
pass
|
|
927
700
|
try:
|
|
928
|
-
await self.
|
|
701
|
+
await self.delete_memory(user_id)
|
|
929
702
|
except Exception:
|
|
930
703
|
pass
|
|
931
704
|
|
|
@@ -941,8 +714,8 @@ class AI:
|
|
|
941
714
|
thread_id = self._database.get_thread_id(user_id)
|
|
942
715
|
await self._client.beta.threads.delete(thread_id=thread_id)
|
|
943
716
|
|
|
944
|
-
async def
|
|
945
|
-
"""Delete
|
|
717
|
+
async def delete_memory(self, user_id: str):
|
|
718
|
+
"""Delete memory for a specific user from Zep memory.
|
|
946
719
|
|
|
947
720
|
Args:
|
|
948
721
|
user_id (str): Unique identifier for the user whose facts should be deleted
|
|
@@ -973,11 +746,11 @@ class AI:
|
|
|
973
746
|
"""Process text input and stream AI responses asynchronously.
|
|
974
747
|
|
|
975
748
|
Args:
|
|
976
|
-
user_id (str): Unique identifier for the user/conversation
|
|
977
|
-
user_text (str): Text input from user to process
|
|
749
|
+
user_id (str): Unique identifier for the user/conversation.
|
|
750
|
+
user_text (str): Text input from user to process.
|
|
978
751
|
|
|
979
752
|
Returns:
|
|
980
|
-
AsyncGenerator[str, None]: Stream of response text chunks
|
|
753
|
+
AsyncGenerator[str, None]: Stream of response text chunks (including tool call results).
|
|
981
754
|
|
|
982
755
|
Example:
|
|
983
756
|
```python
|
|
@@ -986,69 +759,109 @@ class AI:
|
|
|
986
759
|
```
|
|
987
760
|
|
|
988
761
|
Note:
|
|
989
|
-
- Maintains conversation thread using OpenAI's thread system
|
|
990
|
-
- Stores messages in configured database (MongoDB/SQLite)
|
|
991
|
-
- Integrates with Zep memory if configured
|
|
992
|
-
-
|
|
993
|
-
- Streams responses for real-time interaction
|
|
762
|
+
- Maintains conversation thread using OpenAI's thread system.
|
|
763
|
+
- Stores messages in configured database (MongoDB/SQLite).
|
|
764
|
+
- Integrates with Zep memory if configured.
|
|
765
|
+
- Supports tool calls by aggregating and executing them as their arguments stream in.
|
|
994
766
|
"""
|
|
995
767
|
self._accumulated_value_queue = asyncio.Queue()
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
if thread_id is None:
|
|
1000
|
-
thread_id = await self._create_thread(user_id)
|
|
1001
|
-
|
|
1002
|
-
self._current_thread_id = thread_id
|
|
1003
|
-
|
|
1004
|
-
# Check for active runs and cancel if necessary
|
|
1005
|
-
active_run_id = await self._get_active_run(thread_id)
|
|
1006
|
-
if active_run_id:
|
|
1007
|
-
await self._cancel_run(thread_id, active_run_id)
|
|
1008
|
-
while await self._get_run_status(thread_id, active_run_id) != "cancelled":
|
|
1009
|
-
await asyncio.sleep(0.1)
|
|
1010
|
-
|
|
1011
|
-
# Create a message in the thread
|
|
1012
|
-
self._client.beta.threads.messages.create(
|
|
1013
|
-
thread_id=thread_id,
|
|
1014
|
-
role="user",
|
|
1015
|
-
content=user_text,
|
|
1016
|
-
)
|
|
1017
|
-
event_handler = EventHandler(self._tool_handlers, self)
|
|
768
|
+
final_tool_calls = {} # Accumulate tool call deltas
|
|
769
|
+
final_response = ""
|
|
1018
770
|
|
|
1019
771
|
async def stream_processor():
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
772
|
+
memory = self.get_memory_context(user_id)
|
|
773
|
+
response = self._client.chat.completions.create(
|
|
774
|
+
model=self._openai_model,
|
|
775
|
+
messages=[
|
|
776
|
+
{
|
|
777
|
+
"role": "system",
|
|
778
|
+
"content": self._instructions + f" Memory: {memory}",
|
|
779
|
+
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
"role": "user",
|
|
783
|
+
"content": user_text,
|
|
784
|
+
},
|
|
785
|
+
],
|
|
786
|
+
tools=self._tools,
|
|
787
|
+
stream=True,
|
|
788
|
+
)
|
|
789
|
+
for chunk in response:
|
|
790
|
+
delta = chunk.choices[0].delta
|
|
791
|
+
|
|
792
|
+
# Process tool call deltas (if any)
|
|
793
|
+
if delta.tool_calls:
|
|
794
|
+
for tool_call in delta.tool_calls:
|
|
795
|
+
index = tool_call.index
|
|
796
|
+
if tool_call.function.name:
|
|
797
|
+
# Initialize a new tool call record
|
|
798
|
+
final_tool_calls[index] = {
|
|
799
|
+
"name": tool_call.function.name,
|
|
800
|
+
"arguments": tool_call.function.arguments or ""
|
|
801
|
+
}
|
|
802
|
+
elif tool_call.function.arguments:
|
|
803
|
+
# Append additional arguments if provided in subsequent chunks
|
|
804
|
+
final_tool_calls[index]["arguments"] += tool_call.function.arguments
|
|
805
|
+
|
|
806
|
+
try:
|
|
807
|
+
args = json.loads(
|
|
808
|
+
final_tool_calls[index]["arguments"])
|
|
809
|
+
func = getattr(
|
|
810
|
+
self, final_tool_calls[index]["name"])
|
|
811
|
+
# Execute the tool call (synchronously; adjust if async is needed)
|
|
812
|
+
result = func(**args)
|
|
813
|
+
response = self._client.chat.completions.create(
|
|
814
|
+
model=self._openai_model,
|
|
815
|
+
messages=[
|
|
816
|
+
{
|
|
817
|
+
"role": "system",
|
|
818
|
+
"content": self._instructions,
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
"role": "user",
|
|
822
|
+
"content": result,
|
|
823
|
+
},
|
|
824
|
+
],
|
|
825
|
+
tools=self._tools,
|
|
826
|
+
stream=True,
|
|
827
|
+
)
|
|
828
|
+
for chunk in response:
|
|
829
|
+
delta = chunk.choices[0].delta
|
|
830
|
+
|
|
831
|
+
if delta.content is not None:
|
|
832
|
+
await self._accumulated_value_queue.put(delta.content)
|
|
833
|
+
# Remove the cached tool call info so it doesn't block future calls
|
|
834
|
+
del final_tool_calls[index]
|
|
835
|
+
except json.JSONDecodeError:
|
|
836
|
+
# If the accumulated arguments aren't valid yet, wait for more chunks.
|
|
837
|
+
continue
|
|
838
|
+
|
|
839
|
+
# Process regular response content
|
|
840
|
+
if delta.content is not None:
|
|
841
|
+
await self._accumulated_value_queue.put(delta.content)
|
|
842
|
+
|
|
843
|
+
# Start the stream processor as a background task
|
|
1028
844
|
asyncio.create_task(stream_processor())
|
|
1029
845
|
|
|
1030
|
-
# Yield values from the queue as they become available
|
|
1031
|
-
full_response = ""
|
|
846
|
+
# Yield values from the queue as they become available.
|
|
1032
847
|
while True:
|
|
1033
848
|
try:
|
|
1034
|
-
value = await asyncio.wait_for(
|
|
1035
|
-
self._accumulated_value_queue.get(), timeout=0.1
|
|
1036
|
-
)
|
|
849
|
+
value = await asyncio.wait_for(self._accumulated_value_queue.get(), timeout=0.1)
|
|
1037
850
|
if value is not None:
|
|
1038
|
-
|
|
851
|
+
final_response += value
|
|
1039
852
|
yield value
|
|
1040
853
|
except asyncio.TimeoutError:
|
|
854
|
+
# Break only if the queue is empty (assuming stream ended)
|
|
1041
855
|
if self._accumulated_value_queue.empty():
|
|
1042
856
|
break
|
|
1043
857
|
|
|
1044
|
-
# Save the
|
|
858
|
+
# Save the conversation to the database and Zep memory (if configured)
|
|
1045
859
|
metadata = {
|
|
1046
860
|
"user_id": user_id,
|
|
1047
861
|
"message": user_text,
|
|
1048
|
-
"response":
|
|
862
|
+
"response": final_response,
|
|
1049
863
|
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
|
1050
864
|
}
|
|
1051
|
-
|
|
1052
865
|
self._database.save_message(user_id, metadata)
|
|
1053
866
|
if self._zep:
|
|
1054
867
|
messages = [
|
|
@@ -1060,7 +873,7 @@ class AI:
|
|
|
1060
873
|
Message(
|
|
1061
874
|
role="assistant",
|
|
1062
875
|
role_type="assistant",
|
|
1063
|
-
content=
|
|
876
|
+
content=final_response,
|
|
1064
877
|
),
|
|
1065
878
|
]
|
|
1066
879
|
await self._zep.memory.add(session_id=user_id, messages=messages)
|
|
@@ -1112,56 +925,105 @@ class AI:
|
|
|
1112
925
|
- Streams audio response using OpenAI TTS
|
|
1113
926
|
"""
|
|
1114
927
|
|
|
1115
|
-
# Reset the queue for each new conversation
|
|
1116
|
-
self._accumulated_value_queue = asyncio.Queue()
|
|
1117
|
-
|
|
1118
|
-
thread_id = self._database.get_thread_id(user_id)
|
|
1119
|
-
|
|
1120
|
-
if thread_id is None:
|
|
1121
|
-
thread_id = await self._create_thread(user_id)
|
|
1122
|
-
|
|
1123
|
-
self._current_thread_id = thread_id
|
|
1124
928
|
transcript = await self._listen(audio_bytes, input_format)
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
role="user",
|
|
1129
|
-
content=transcript,
|
|
1130
|
-
)
|
|
929
|
+
self._accumulated_value_queue = asyncio.Queue()
|
|
930
|
+
final_tool_calls = {} # Accumulate tool call deltas
|
|
931
|
+
final_response = ""
|
|
1131
932
|
|
|
1132
933
|
async def stream_processor():
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
934
|
+
memory = self.get_memory_context(user_id)
|
|
935
|
+
response = self._client.chat.completions.create(
|
|
936
|
+
model=self._openai_model,
|
|
937
|
+
messages=[
|
|
938
|
+
{
|
|
939
|
+
"role": "system",
|
|
940
|
+
"content": self._instructions + f" Memory: {memory}",
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
"role": "user",
|
|
944
|
+
"content": transcript,
|
|
945
|
+
},
|
|
946
|
+
],
|
|
947
|
+
tools=self._tools,
|
|
948
|
+
stream=True,
|
|
949
|
+
)
|
|
950
|
+
for chunk in response:
|
|
951
|
+
delta = chunk.choices[0].delta
|
|
952
|
+
|
|
953
|
+
# Process tool call deltas (if any)
|
|
954
|
+
if delta.tool_calls:
|
|
955
|
+
for tool_call in delta.tool_calls:
|
|
956
|
+
index = tool_call.index
|
|
957
|
+
if tool_call.function.name:
|
|
958
|
+
# Initialize a new tool call record
|
|
959
|
+
final_tool_calls[index] = {
|
|
960
|
+
"name": tool_call.function.name,
|
|
961
|
+
"arguments": tool_call.function.arguments or ""
|
|
962
|
+
}
|
|
963
|
+
elif tool_call.function.arguments:
|
|
964
|
+
# Append additional arguments if provided in subsequent chunks
|
|
965
|
+
final_tool_calls[index]["arguments"] += tool_call.function.arguments
|
|
966
|
+
|
|
967
|
+
try:
|
|
968
|
+
args = json.loads(
|
|
969
|
+
final_tool_calls[index]["arguments"])
|
|
970
|
+
func = getattr(
|
|
971
|
+
self, final_tool_calls[index]["name"])
|
|
972
|
+
# Execute the tool call (synchronously; adjust if async is needed)
|
|
973
|
+
result = func(**args)
|
|
974
|
+
response = self._client.chat.completions.create(
|
|
975
|
+
model=self._openai_model,
|
|
976
|
+
messages=[
|
|
977
|
+
{
|
|
978
|
+
"role": "system",
|
|
979
|
+
"content": self._instructions,
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
"role": "user",
|
|
983
|
+
"content": result,
|
|
984
|
+
},
|
|
985
|
+
],
|
|
986
|
+
tools=self._tools,
|
|
987
|
+
stream=True,
|
|
988
|
+
)
|
|
989
|
+
for chunk in response:
|
|
990
|
+
delta = chunk.choices[0].delta
|
|
991
|
+
|
|
992
|
+
if delta.content is not None:
|
|
993
|
+
await self._accumulated_value_queue.put(delta.content)
|
|
994
|
+
# Remove the cached tool call info so it doesn't block future calls
|
|
995
|
+
del final_tool_calls[index]
|
|
996
|
+
except json.JSONDecodeError:
|
|
997
|
+
# If the accumulated arguments aren't valid yet, wait for more chunks.
|
|
998
|
+
continue
|
|
999
|
+
|
|
1000
|
+
# Process regular response content
|
|
1001
|
+
if delta.content is not None:
|
|
1002
|
+
await self._accumulated_value_queue.put(delta.content)
|
|
1003
|
+
|
|
1004
|
+
# Start the stream processor as a background task
|
|
1141
1005
|
asyncio.create_task(stream_processor())
|
|
1142
1006
|
|
|
1143
|
-
#
|
|
1144
|
-
full_response = ""
|
|
1007
|
+
# Yield values from the queue as they become available.
|
|
1145
1008
|
while True:
|
|
1146
1009
|
try:
|
|
1147
|
-
value = await asyncio.wait_for(
|
|
1148
|
-
self._accumulated_value_queue.get(), timeout=0.1
|
|
1149
|
-
)
|
|
1010
|
+
value = await asyncio.wait_for(self._accumulated_value_queue.get(), timeout=0.1)
|
|
1150
1011
|
if value is not None:
|
|
1151
|
-
|
|
1012
|
+
final_response += value
|
|
1013
|
+
yield value
|
|
1152
1014
|
except asyncio.TimeoutError:
|
|
1015
|
+
# Break only if the queue is empty (assuming stream ended)
|
|
1153
1016
|
if self._accumulated_value_queue.empty():
|
|
1154
1017
|
break
|
|
1155
1018
|
|
|
1019
|
+
# Save the conversation to the database and Zep memory (if configured)
|
|
1156
1020
|
metadata = {
|
|
1157
1021
|
"user_id": user_id,
|
|
1158
1022
|
"message": transcript,
|
|
1159
|
-
"response":
|
|
1023
|
+
"response": final_response,
|
|
1160
1024
|
"timestamp": datetime.datetime.now(datetime.timezone.utc),
|
|
1161
1025
|
}
|
|
1162
|
-
|
|
1163
1026
|
self._database.save_message(user_id, metadata)
|
|
1164
|
-
|
|
1165
1027
|
if self._zep:
|
|
1166
1028
|
messages = [
|
|
1167
1029
|
Message(
|
|
@@ -1172,7 +1034,7 @@ class AI:
|
|
|
1172
1034
|
Message(
|
|
1173
1035
|
role="assistant",
|
|
1174
1036
|
role_type="assistant",
|
|
1175
|
-
content=
|
|
1037
|
+
content=final_response,
|
|
1176
1038
|
),
|
|
1177
1039
|
]
|
|
1178
1040
|
await self._zep.memory.add(session_id=user_id, messages=messages)
|
|
@@ -1181,32 +1043,12 @@ class AI:
|
|
|
1181
1043
|
with self._client.audio.speech.with_streaming_response.create(
|
|
1182
1044
|
model="tts-1",
|
|
1183
1045
|
voice=voice,
|
|
1184
|
-
input=
|
|
1046
|
+
input=final_response,
|
|
1185
1047
|
response_format=response_format,
|
|
1186
1048
|
) as response:
|
|
1187
1049
|
for chunk in response.iter_bytes(1024):
|
|
1188
1050
|
yield chunk
|
|
1189
1051
|
|
|
1190
|
-
def _handle_requires_action(self, data, run_id):
|
|
1191
|
-
tool_outputs = []
|
|
1192
|
-
|
|
1193
|
-
for tool in data.required_action.submit_tool_outputs.tool_calls:
|
|
1194
|
-
if tool.function.name in self._tool_handlers:
|
|
1195
|
-
handler = self._tool_handlers[tool.function.name]
|
|
1196
|
-
inputs = json.loads(tool.function.arguments)
|
|
1197
|
-
output = handler(**inputs)
|
|
1198
|
-
tool_outputs.append(
|
|
1199
|
-
{"tool_call_id": tool.id, "output": output})
|
|
1200
|
-
|
|
1201
|
-
self._submit_tool_outputs(tool_outputs, run_id)
|
|
1202
|
-
|
|
1203
|
-
def _submit_tool_outputs(self, tool_outputs, run_id):
|
|
1204
|
-
with self._client.beta.threads.runs.submit_tool_outputs_stream(
|
|
1205
|
-
thread_id=self._current_thread_id, run_id=run_id, tool_outputs=tool_outputs
|
|
1206
|
-
) as stream:
|
|
1207
|
-
for text in stream.text_deltas:
|
|
1208
|
-
asyncio.create_task(self._accumulated_value_queue.put(text))
|
|
1209
|
-
|
|
1210
1052
|
def add_tool(self, func: Callable):
|
|
1211
1053
|
"""Register a custom function as an AI tool using decorator pattern.
|
|
1212
1054
|
|
|
@@ -1253,7 +1095,8 @@ class AI:
|
|
|
1253
1095
|
},
|
|
1254
1096
|
}
|
|
1255
1097
|
self._tools.append(tool_config)
|
|
1256
|
-
|
|
1098
|
+
# Attach the function to the instance so getattr can find it later.
|
|
1099
|
+
setattr(self, func.__name__, func)
|
|
1257
1100
|
return func
|
|
1258
1101
|
|
|
1259
1102
|
|
|
File without changes
|
|
File without changes
|