solana-agent 0.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.
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2024 Bevan Hunt
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: solana-agent
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: The Best AI Agent Framework
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: ai,openai,ai agents
|
|
7
|
+
Author: Bevan Hunt
|
|
8
|
+
Author-email: bevan@bevanhunt.com
|
|
9
|
+
Requires-Python: >=3.9,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Dist: aiosqlite (>=0.21.0,<0.22.0)
|
|
20
|
+
Requires-Dist: motor (>=3.7.0,<4.0.0)
|
|
21
|
+
Requires-Dist: openai (>=1.61.1,<2.0.0)
|
|
22
|
+
Requires-Dist: pydantic (>=2.10.6,<3.0.0)
|
|
23
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
24
|
+
Requires-Dist: zep-python (>=2.0.2,<3.0.0)
|
|
25
|
+
Project-URL: Repository, https://github.com/truemagic-coder/solana-agent
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Solana-Agent
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/solana-agent/)
|
|
31
|
+
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
Solana Agent is the best AI Agent framework.
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- Streaming text-based conversations with AI
|
|
39
|
+
- Audio transcription and streaming text-to-speech conversion
|
|
40
|
+
- Thread management for maintaining conversation context
|
|
41
|
+
- Message persistence using SQLite or MongoDB
|
|
42
|
+
- Custom tool integration for extending AI capabilities
|
|
43
|
+
- The best memory context currently available for AI Agents
|
|
44
|
+
- Zep integration for tracking facts
|
|
45
|
+
- Search Internet with Perplexity tool
|
|
46
|
+
- Search Zep facts tool
|
|
47
|
+
- Search X with Grok tool
|
|
48
|
+
- Reasoning tool that combines OpenAI model reasoning, Zep facts, Internet search, and X search.
|
|
49
|
+
- Solana tools upcoming...
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
You can install Solana Agent using pip:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install solana-agent
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
Here's a basic example of how to use Solana Agent:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from solana-agent import AI, SQLiteDatabase
|
|
65
|
+
|
|
66
|
+
async def main():
|
|
67
|
+
database = SQLiteDatabase("conversations.db")
|
|
68
|
+
async with AI("your_openai_api_key", "AI Assistant", "Your instructions here", database) as ai:
|
|
69
|
+
user_id = "user123"
|
|
70
|
+
response = await ai.text(user_id, "Hello, AI!")
|
|
71
|
+
async for chunk in response:
|
|
72
|
+
print(chunk, end="", flush=True)
|
|
73
|
+
print()
|
|
74
|
+
|
|
75
|
+
# Run the async main function
|
|
76
|
+
import asyncio
|
|
77
|
+
asyncio.run(main())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Contributing
|
|
81
|
+
|
|
82
|
+
Contributions to Solana Agent are welcome! Please feel free to submit a Pull Request.
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
87
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Solana-Agent
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/solana-agent/)
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Solana Agent is the best AI Agent framework.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Streaming text-based conversations with AI
|
|
12
|
+
- Audio transcription and streaming text-to-speech conversion
|
|
13
|
+
- Thread management for maintaining conversation context
|
|
14
|
+
- Message persistence using SQLite or MongoDB
|
|
15
|
+
- Custom tool integration for extending AI capabilities
|
|
16
|
+
- The best memory context currently available for AI Agents
|
|
17
|
+
- Zep integration for tracking facts
|
|
18
|
+
- Search Internet with Perplexity tool
|
|
19
|
+
- Search Zep facts tool
|
|
20
|
+
- Search X with Grok tool
|
|
21
|
+
- Reasoning tool that combines OpenAI model reasoning, Zep facts, Internet search, and X search.
|
|
22
|
+
- Solana tools upcoming...
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
You can install Solana Agent using pip:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install solana-agent
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
Here's a basic example of how to use Solana Agent:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from solana-agent import AI, SQLiteDatabase
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
database = SQLiteDatabase("conversations.db")
|
|
41
|
+
async with AI("your_openai_api_key", "AI Assistant", "Your instructions here", database) as ai:
|
|
42
|
+
user_id = "user123"
|
|
43
|
+
response = await ai.text(user_id, "Hello, AI!")
|
|
44
|
+
async for chunk in response:
|
|
45
|
+
print(chunk, end="", flush=True)
|
|
46
|
+
print()
|
|
47
|
+
|
|
48
|
+
# Run the async main function
|
|
49
|
+
import asyncio
|
|
50
|
+
asyncio.run(main())
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Contributing
|
|
54
|
+
|
|
55
|
+
Contributions to Solana Agent are welcome! Please feel free to submit a Pull Request.
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "solana-agent"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "The Best AI Agent Framework"
|
|
5
|
+
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
repository = "https://github.com/truemagic-coder/solana-agent"
|
|
9
|
+
keywords = ["ai", "openai", "ai agents"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
12
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
13
|
+
]
|
|
14
|
+
packages = [{ include = "solana_agent" }]
|
|
15
|
+
|
|
16
|
+
[tool.pytest.ini-options]
|
|
17
|
+
python_paths = [".", "tests"]
|
|
18
|
+
|
|
19
|
+
[tool.poetry.dependencies]
|
|
20
|
+
python = ">=3.9,<4.0"
|
|
21
|
+
openai = "^1.61.1"
|
|
22
|
+
pydantic = "^2.10.6"
|
|
23
|
+
motor = "^3.7.0"
|
|
24
|
+
aiosqlite = "^0.21.0"
|
|
25
|
+
zep-python = "^2.0.2"
|
|
26
|
+
requests = "^2.32.3"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["poetry-core>=1.0.0"]
|
|
30
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .ai import *
|
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import json
|
|
4
|
+
from typing import AsyncGenerator, List, Literal, Optional, Dict, Any, Callable
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
|
7
|
+
from openai import OpenAI
|
|
8
|
+
import openai
|
|
9
|
+
import aiosqlite
|
|
10
|
+
from openai import AssistantEventHandler
|
|
11
|
+
from openai.types.beta.threads import TextDelta, Text
|
|
12
|
+
from typing_extensions import override
|
|
13
|
+
import sqlite3
|
|
14
|
+
import inspect
|
|
15
|
+
import requests
|
|
16
|
+
from zep_python.client import AsyncZep
|
|
17
|
+
from zep_python.client import Zep
|
|
18
|
+
from zep_python.types import Message, RoleType
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def adapt_datetime(ts):
|
|
22
|
+
return ts.isoformat()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Custom converter for datetime
|
|
26
|
+
def convert_datetime(ts):
|
|
27
|
+
return datetime.fromisoformat(ts)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Register the adapter and converter
|
|
31
|
+
sqlite3.register_adapter(datetime, adapt_datetime)
|
|
32
|
+
sqlite3.register_converter("timestamp", convert_datetime)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class EventHandler(AssistantEventHandler):
|
|
36
|
+
def __init__(self, tool_handlers, ai_instance):
|
|
37
|
+
super().__init__()
|
|
38
|
+
self.tool_handlers = tool_handlers
|
|
39
|
+
self.ai_instance = ai_instance
|
|
40
|
+
|
|
41
|
+
@override
|
|
42
|
+
def on_text_delta(self, delta: TextDelta, snapshot: Text):
|
|
43
|
+
asyncio.create_task(
|
|
44
|
+
self.ai_instance.accumulated_value_queue.put(delta.value))
|
|
45
|
+
|
|
46
|
+
@override
|
|
47
|
+
def on_event(self, event):
|
|
48
|
+
if event.event == "thread.run.requires_action":
|
|
49
|
+
run_id = event.data.id
|
|
50
|
+
self.ai_instance.handle_requires_action(event.data, run_id)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ToolConfig(BaseModel):
|
|
54
|
+
name: str
|
|
55
|
+
description: str
|
|
56
|
+
parameters: Dict[str, Any]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class MongoDatabase:
|
|
60
|
+
def __init__(self, db_url: str, db_name: str):
|
|
61
|
+
self.client = AsyncIOMotorClient(db_url)
|
|
62
|
+
self.db = self.client[db_name]
|
|
63
|
+
self.threads = self.db["threads"]
|
|
64
|
+
self.messages = self.db["messages"]
|
|
65
|
+
|
|
66
|
+
async def save_thread_id(self, user_id: str, thread_id: str):
|
|
67
|
+
await self.threads.insert_one({"thread_id": thread_id, "user_id": user_id})
|
|
68
|
+
|
|
69
|
+
async def get_thread_id(self, user_id: str) -> Optional[str]:
|
|
70
|
+
document = await self.threads.find_one({"user_id": user_id})
|
|
71
|
+
return document["thread_id"] if document else None
|
|
72
|
+
|
|
73
|
+
async def save_message(self, user_id: str, metadata: Dict[str, Any]):
|
|
74
|
+
metadata["user_id"] = user_id
|
|
75
|
+
await self.messages.insert_one(metadata)
|
|
76
|
+
|
|
77
|
+
async def delete_thread_id(self, user_id: str):
|
|
78
|
+
document = await self.threads.find_one({"user_id": user_id})
|
|
79
|
+
thread_id = document["thread_id"]
|
|
80
|
+
openai.beta.threads.delete(thread_id)
|
|
81
|
+
await self.messages.delete_many({"user_id": user_id})
|
|
82
|
+
await self.threads.delete_one({"user_id": user_id})
|
|
83
|
+
|
|
84
|
+
async def delete_all_threads(self):
|
|
85
|
+
await self.threads.delete_many({})
|
|
86
|
+
await self.messages.delete_many({})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class SQLiteDatabase:
|
|
90
|
+
def __init__(self, db_path: str):
|
|
91
|
+
self.db_path = db_path
|
|
92
|
+
self.conn = sqlite3.connect(db_path)
|
|
93
|
+
self.conn.execute(
|
|
94
|
+
"CREATE TABLE IF NOT EXISTS threads (user_id TEXT, thread_id TEXT)"
|
|
95
|
+
)
|
|
96
|
+
self.conn.execute(
|
|
97
|
+
"CREATE TABLE IF NOT EXISTS messages (user_id TEXT, message TEXT, response TEXT, timestamp TEXT)"
|
|
98
|
+
)
|
|
99
|
+
self.conn.commit()
|
|
100
|
+
self.conn.close()
|
|
101
|
+
|
|
102
|
+
async def save_thread_id(self, user_id: str, thread_id: str):
|
|
103
|
+
async with aiosqlite.connect(
|
|
104
|
+
self.db_path, detect_types=sqlite3.PARSE_DECLTYPES
|
|
105
|
+
) as db:
|
|
106
|
+
await db.execute(
|
|
107
|
+
"INSERT INTO threads (user_id, thread_id) VALUES (?, ?)",
|
|
108
|
+
(user_id, thread_id),
|
|
109
|
+
)
|
|
110
|
+
await db.commit()
|
|
111
|
+
|
|
112
|
+
async def get_thread_id(self, user_id: str) -> Optional[str]:
|
|
113
|
+
async with aiosqlite.connect(
|
|
114
|
+
self.db_path, detect_types=sqlite3.PARSE_DECLTYPES
|
|
115
|
+
) as db:
|
|
116
|
+
async with db.execute(
|
|
117
|
+
"SELECT thread_id FROM threads WHERE user_id = ?", (user_id,)
|
|
118
|
+
) as cursor:
|
|
119
|
+
row = await cursor.fetchone()
|
|
120
|
+
return row[0] if row else None
|
|
121
|
+
|
|
122
|
+
async def save_message(self, user_id: str, metadata: Dict[str, Any]):
|
|
123
|
+
async with aiosqlite.connect(
|
|
124
|
+
self.db_path, detect_types=sqlite3.PARSE_DECLTYPES
|
|
125
|
+
) as db:
|
|
126
|
+
await db.execute(
|
|
127
|
+
"INSERT INTO messages (user_id, message, response, timestamp) VALUES (?, ?, ?, ?)",
|
|
128
|
+
(
|
|
129
|
+
user_id,
|
|
130
|
+
metadata["message"],
|
|
131
|
+
metadata["response"],
|
|
132
|
+
metadata["timestamp"],
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
await db.commit()
|
|
136
|
+
|
|
137
|
+
async def delete_thread_id(self, user_id: str):
|
|
138
|
+
async with aiosqlite.connect(
|
|
139
|
+
self.db_path, detect_types=sqlite3.PARSE_DECLTYPES
|
|
140
|
+
) as db:
|
|
141
|
+
async with db.execute(
|
|
142
|
+
"SELECT thread_id FROM threads WHERE user_id = ?", (user_id,)
|
|
143
|
+
) as cursor:
|
|
144
|
+
row = await cursor.fetchone()
|
|
145
|
+
if row:
|
|
146
|
+
thread_id = row[0]
|
|
147
|
+
openai.beta.threads.delete(thread_id)
|
|
148
|
+
await db.execute(
|
|
149
|
+
"DELETE FROM messages WHERE user_id = ?", (user_id,)
|
|
150
|
+
)
|
|
151
|
+
await db.execute(
|
|
152
|
+
"DELETE FROM threads WHERE user_id = ?", (user_id,)
|
|
153
|
+
)
|
|
154
|
+
await db.commit()
|
|
155
|
+
|
|
156
|
+
async def delete_all_threads(self):
|
|
157
|
+
async with aiosqlite.connect(
|
|
158
|
+
self.db_path, detect_types=sqlite3.PARSE_DECLTYPES
|
|
159
|
+
) as db:
|
|
160
|
+
await db.execute("DELETE FROM messages")
|
|
161
|
+
await db.execute("DELETE FROM threads")
|
|
162
|
+
await db.commit()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class AI:
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
openai_api_key: str,
|
|
169
|
+
name: str,
|
|
170
|
+
instructions: str,
|
|
171
|
+
database: Any,
|
|
172
|
+
zep_api_key: str = None,
|
|
173
|
+
zep_base_url: str = None,
|
|
174
|
+
perplexity_api_key: str = None,
|
|
175
|
+
grok_api_key: str = None,
|
|
176
|
+
code_interpreter: bool = True,
|
|
177
|
+
model: Literal["gpt-4o-mini", "gpt-4o"] = "gpt-4o-mini",
|
|
178
|
+
):
|
|
179
|
+
self.client = OpenAI(api_key=openai_api_key)
|
|
180
|
+
self.name = name
|
|
181
|
+
self.instructions = instructions
|
|
182
|
+
self.model = model
|
|
183
|
+
self.tools = [{"type": "code_interpreter"}] if code_interpreter else []
|
|
184
|
+
self.tool_handlers = {}
|
|
185
|
+
self.assistant_id = None
|
|
186
|
+
self.database = database
|
|
187
|
+
self.accumulated_value_queue = asyncio.Queue()
|
|
188
|
+
self.zep = (
|
|
189
|
+
AsyncZep(api_key=zep_api_key, base_url=zep_base_url)
|
|
190
|
+
if zep_api_key
|
|
191
|
+
else None
|
|
192
|
+
)
|
|
193
|
+
self.sync_zep = (
|
|
194
|
+
Zep(api_key=zep_api_key, base_url=zep_base_url) if zep_api_key else None
|
|
195
|
+
)
|
|
196
|
+
self.perplexity_api_key = perplexity_api_key
|
|
197
|
+
self.grok_api_key = grok_api_key
|
|
198
|
+
|
|
199
|
+
async def __aenter__(self):
|
|
200
|
+
assistants = openai.beta.assistants.list()
|
|
201
|
+
existing_assistant = next(
|
|
202
|
+
(a for a in assistants if a.name == self.name), None)
|
|
203
|
+
|
|
204
|
+
if existing_assistant:
|
|
205
|
+
self.assistant_id = existing_assistant.id
|
|
206
|
+
else:
|
|
207
|
+
self.assistant_id = openai.beta.assistants.create(
|
|
208
|
+
name=self.name,
|
|
209
|
+
instructions=self.instructions,
|
|
210
|
+
tools=self.tools,
|
|
211
|
+
model=self.model,
|
|
212
|
+
).id
|
|
213
|
+
await self.database.delete_all_threads()
|
|
214
|
+
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
218
|
+
# Perform any cleanup actions here
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
async def create_thread(self, user_id: str) -> str:
|
|
222
|
+
thread_id = await self.database.get_thread_id(user_id)
|
|
223
|
+
|
|
224
|
+
if thread_id is None:
|
|
225
|
+
thread = openai.beta.threads.create()
|
|
226
|
+
thread_id = thread.id
|
|
227
|
+
await self.database.save_thread_id(user_id, thread_id)
|
|
228
|
+
if self.zep:
|
|
229
|
+
await self.zep.user.add(user_id=user_id)
|
|
230
|
+
await self.zep.memory.add_session(user_id=user_id, session_id=user_id)
|
|
231
|
+
|
|
232
|
+
return thread_id
|
|
233
|
+
|
|
234
|
+
async def cancel_run(self, thread_id: str, run_id: str):
|
|
235
|
+
try:
|
|
236
|
+
self.client.beta.threads.runs.cancel(
|
|
237
|
+
thread_id=thread_id, run_id=run_id)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
print(f"Error cancelling run: {e}")
|
|
240
|
+
|
|
241
|
+
async def get_active_run(self, thread_id: str) -> Optional[str]:
|
|
242
|
+
runs = self.client.beta.threads.runs.list(thread_id=thread_id, limit=1)
|
|
243
|
+
for run in runs:
|
|
244
|
+
if run.status in ["in_progress"]:
|
|
245
|
+
return run.id
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
async def get_run_status(self, thread_id: str, run_id: str) -> str:
|
|
249
|
+
run = self.client.beta.threads.runs.retrieve(
|
|
250
|
+
thread_id=thread_id, run_id=run_id)
|
|
251
|
+
return run.status
|
|
252
|
+
|
|
253
|
+
# search facts tool - has to be sync
|
|
254
|
+
def search_facts(
|
|
255
|
+
self,
|
|
256
|
+
user_id: str,
|
|
257
|
+
query: str,
|
|
258
|
+
limit: int | None = None,
|
|
259
|
+
) -> List[str] | None:
|
|
260
|
+
if self.sync_zep:
|
|
261
|
+
facts = []
|
|
262
|
+
results = self.sync_zep.memory.search_sessions(
|
|
263
|
+
user_id=user_id,
|
|
264
|
+
text=query,
|
|
265
|
+
limit=limit,
|
|
266
|
+
)
|
|
267
|
+
for result in results.results:
|
|
268
|
+
fact = result.fact.fact
|
|
269
|
+
if fact:
|
|
270
|
+
facts.append(fact)
|
|
271
|
+
return facts
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
# search internet tool - has to be sync
|
|
275
|
+
def search_internet(
|
|
276
|
+
self,
|
|
277
|
+
query: str,
|
|
278
|
+
model: Literal[
|
|
279
|
+
"sonar", "sonar-pro", "sonar-reasoning-pro", "sonar-reasoning"
|
|
280
|
+
] = "sonar",
|
|
281
|
+
) -> str:
|
|
282
|
+
try:
|
|
283
|
+
url = "https://api.perplexity.ai/chat/completions"
|
|
284
|
+
|
|
285
|
+
payload = {
|
|
286
|
+
"model": model,
|
|
287
|
+
"messages": [
|
|
288
|
+
{
|
|
289
|
+
"role": "system",
|
|
290
|
+
"content": "You answer the user's query.",
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"role": "user",
|
|
294
|
+
"content": query,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
}
|
|
298
|
+
headers = {
|
|
299
|
+
"Authorization": f"Bearer {self.perplexity_api_key}",
|
|
300
|
+
"Content-Type": "application/json",
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
response = requests.post(url, json=payload, headers=headers)
|
|
304
|
+
if response.status_code == 200:
|
|
305
|
+
data = response.json()
|
|
306
|
+
content = data["choices"][0]["message"]["content"]
|
|
307
|
+
return content
|
|
308
|
+
else:
|
|
309
|
+
return (
|
|
310
|
+
f"Failed to search Perplexity. Status code: {response.status_code}"
|
|
311
|
+
)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
return f"Failed to search Perplexity. Error: {e}"
|
|
314
|
+
|
|
315
|
+
# reason tool - has to be sync
|
|
316
|
+
def reason(
|
|
317
|
+
self,
|
|
318
|
+
user_id: str,
|
|
319
|
+
query: str,
|
|
320
|
+
perplexity_model: Literal[
|
|
321
|
+
"sonar", "sonar-pro", "sonar-reasoning-pro", "sonar-reasoning"
|
|
322
|
+
] = "sonar",
|
|
323
|
+
openai_model: Literal["o1", "o3-mini"] = "o3-mini",
|
|
324
|
+
grok_model: Literal["grok-beta"] = "grok-beta",
|
|
325
|
+
) -> str:
|
|
326
|
+
try:
|
|
327
|
+
facts = self.search_facts(user_id, query)
|
|
328
|
+
if not facts:
|
|
329
|
+
facts = ""
|
|
330
|
+
search_results = self.search_internet(query, perplexity_model)
|
|
331
|
+
x_search_results = self.search_x(query, grok_model)
|
|
332
|
+
|
|
333
|
+
response = self.client.chat.completions.create(
|
|
334
|
+
model=openai_model,
|
|
335
|
+
messages=[
|
|
336
|
+
{
|
|
337
|
+
"role": "system",
|
|
338
|
+
"content": "You combine the data with your reasoning to answer the query.",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"role": "user",
|
|
342
|
+
"content": f"Query: {query}, Facts: {facts}, Internet Search Results: {search_results}, X Search Results: {x_search_results}",
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
)
|
|
346
|
+
return response.choices[0].message.content
|
|
347
|
+
except Exception as e:
|
|
348
|
+
return f"Failed to reason. Error: {e}"
|
|
349
|
+
|
|
350
|
+
# x search tool - has to be sync
|
|
351
|
+
def search_x(self, query: str, model: Literal["grok-beta"] = "grok-beta") -> str:
|
|
352
|
+
try:
|
|
353
|
+
client = OpenAI(api_key=self.grok_api_key,
|
|
354
|
+
base_url="https://api.x.ai/v1")
|
|
355
|
+
|
|
356
|
+
completion = client.chat.completions.create(
|
|
357
|
+
model=model,
|
|
358
|
+
messages=[
|
|
359
|
+
{
|
|
360
|
+
"role": "system",
|
|
361
|
+
"content": "You answer the user's query.",
|
|
362
|
+
},
|
|
363
|
+
{"role": "user", "content": query},
|
|
364
|
+
],
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
return completion.choices[0].message.content
|
|
368
|
+
except Exception as e:
|
|
369
|
+
return f"Failed to search X. Error: {e}"
|
|
370
|
+
|
|
371
|
+
async def delete_facts(self, user_id: str):
|
|
372
|
+
if self.zep:
|
|
373
|
+
await self.zep.memory.delete(session_id=user_id)
|
|
374
|
+
|
|
375
|
+
async def listen(self, audio_content: bytes, input_format: str) -> str:
|
|
376
|
+
transcription = self.client.audio.transcriptions.create(
|
|
377
|
+
model="whisper-1",
|
|
378
|
+
file=(f"file.{input_format}", audio_content),
|
|
379
|
+
)
|
|
380
|
+
return transcription.text
|
|
381
|
+
|
|
382
|
+
async def text(self, user_id: str, user_text: str) -> AsyncGenerator[str, None]:
|
|
383
|
+
self.accumulated_value_queue = asyncio.Queue()
|
|
384
|
+
|
|
385
|
+
thread_id = await self.database.get_thread_id(user_id)
|
|
386
|
+
|
|
387
|
+
if thread_id is None:
|
|
388
|
+
thread_id = await self.create_thread(user_id)
|
|
389
|
+
|
|
390
|
+
self.current_thread_id = thread_id
|
|
391
|
+
|
|
392
|
+
# Check for active runs and cancel if necessary
|
|
393
|
+
active_run_id = await self.get_active_run(thread_id)
|
|
394
|
+
if active_run_id:
|
|
395
|
+
await self.cancel_run(thread_id, active_run_id)
|
|
396
|
+
while await self.get_run_status(thread_id, active_run_id) != "cancelled":
|
|
397
|
+
await asyncio.sleep(0.1)
|
|
398
|
+
|
|
399
|
+
# Create a message in the thread
|
|
400
|
+
self.client.beta.threads.messages.create(
|
|
401
|
+
thread_id=thread_id,
|
|
402
|
+
role="user",
|
|
403
|
+
content=user_text,
|
|
404
|
+
)
|
|
405
|
+
event_handler = EventHandler(self.tool_handlers, self)
|
|
406
|
+
|
|
407
|
+
async def stream_processor():
|
|
408
|
+
with self.client.beta.threads.runs.stream(
|
|
409
|
+
thread_id=thread_id,
|
|
410
|
+
assistant_id=self.assistant_id,
|
|
411
|
+
event_handler=event_handler,
|
|
412
|
+
) as stream:
|
|
413
|
+
stream.until_done()
|
|
414
|
+
|
|
415
|
+
# Start the stream processor in a separate task
|
|
416
|
+
asyncio.create_task(stream_processor())
|
|
417
|
+
|
|
418
|
+
# Yield values from the queue as they become available
|
|
419
|
+
full_response = ""
|
|
420
|
+
while True:
|
|
421
|
+
try:
|
|
422
|
+
value = await asyncio.wait_for(
|
|
423
|
+
self.accumulated_value_queue.get(), timeout=0.1
|
|
424
|
+
)
|
|
425
|
+
if value is not None:
|
|
426
|
+
full_response += value
|
|
427
|
+
yield value
|
|
428
|
+
except asyncio.TimeoutError:
|
|
429
|
+
if self.accumulated_value_queue.empty():
|
|
430
|
+
break
|
|
431
|
+
|
|
432
|
+
# Save the message to the database
|
|
433
|
+
metadata = {
|
|
434
|
+
"user_id": user_id,
|
|
435
|
+
"message": user_text,
|
|
436
|
+
"response": full_response,
|
|
437
|
+
"timestamp": datetime.now(),
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
await self.database.save_message(user_id, metadata)
|
|
441
|
+
if self.zep:
|
|
442
|
+
messages = [
|
|
443
|
+
Message(
|
|
444
|
+
role="user",
|
|
445
|
+
role_type=RoleType["user"],
|
|
446
|
+
content=user_text,
|
|
447
|
+
),
|
|
448
|
+
Message(
|
|
449
|
+
role="assistant",
|
|
450
|
+
role_type=RoleType["assistant"],
|
|
451
|
+
content=full_response,
|
|
452
|
+
),
|
|
453
|
+
]
|
|
454
|
+
await self.zep.memory.add(
|
|
455
|
+
user_id=user_id, session_id=user_id, messages=messages
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
async def conversation(
|
|
459
|
+
self,
|
|
460
|
+
user_id: str,
|
|
461
|
+
audio_bytes: bytes,
|
|
462
|
+
voice: Literal["alloy", "echo", "fable",
|
|
463
|
+
"onyx", "nova", "shimmer"] = "nova",
|
|
464
|
+
input_format: Literal[
|
|
465
|
+
"flac", "m4a", "mp3", "mp4", "mpeg", "mpga", "oga", "ogg", "wav", "webm"
|
|
466
|
+
] = "mp4",
|
|
467
|
+
response_format: Literal["mp3", "opus",
|
|
468
|
+
"aac", "flac", "wav", "pcm"] = "aac",
|
|
469
|
+
) -> AsyncGenerator[bytes, None]:
|
|
470
|
+
# Reset the queue for each new conversation
|
|
471
|
+
self.accumulated_value_queue = asyncio.Queue()
|
|
472
|
+
|
|
473
|
+
thread_id = await self.database.get_thread_id(user_id)
|
|
474
|
+
|
|
475
|
+
if thread_id is None:
|
|
476
|
+
thread_id = await self.create_thread(user_id)
|
|
477
|
+
|
|
478
|
+
self.current_thread_id = thread_id
|
|
479
|
+
transcript = await self.listen(audio_bytes, input_format)
|
|
480
|
+
event_handler = EventHandler(self.tool_handlers, self)
|
|
481
|
+
openai.beta.threads.messages.create(
|
|
482
|
+
thread_id=thread_id,
|
|
483
|
+
role="user",
|
|
484
|
+
content=transcript,
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
async def stream_processor():
|
|
488
|
+
with openai.beta.threads.runs.stream(
|
|
489
|
+
thread_id=thread_id,
|
|
490
|
+
assistant_id=self.assistant_id,
|
|
491
|
+
event_handler=event_handler,
|
|
492
|
+
) as stream:
|
|
493
|
+
stream.until_done()
|
|
494
|
+
|
|
495
|
+
# Start the stream processor in a separate task
|
|
496
|
+
asyncio.create_task(stream_processor())
|
|
497
|
+
|
|
498
|
+
# Collect the full response
|
|
499
|
+
full_response = ""
|
|
500
|
+
while True:
|
|
501
|
+
try:
|
|
502
|
+
value = await asyncio.wait_for(
|
|
503
|
+
self.accumulated_value_queue.get(), timeout=0.1
|
|
504
|
+
)
|
|
505
|
+
if value is not None:
|
|
506
|
+
full_response += value
|
|
507
|
+
except asyncio.TimeoutError:
|
|
508
|
+
if self.accumulated_value_queue.empty():
|
|
509
|
+
break
|
|
510
|
+
|
|
511
|
+
metadata = {
|
|
512
|
+
"user_id": user_id,
|
|
513
|
+
"message": transcript,
|
|
514
|
+
"response": full_response,
|
|
515
|
+
"timestamp": datetime.now(),
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
await self.database.save_message(user_id, metadata)
|
|
519
|
+
|
|
520
|
+
if self.zep:
|
|
521
|
+
messages = [
|
|
522
|
+
Message(
|
|
523
|
+
role="user",
|
|
524
|
+
role_type=RoleType["user"],
|
|
525
|
+
content=transcript,
|
|
526
|
+
),
|
|
527
|
+
Message(
|
|
528
|
+
role="assistant",
|
|
529
|
+
role_type=RoleType["assistant"],
|
|
530
|
+
content=full_response,
|
|
531
|
+
),
|
|
532
|
+
]
|
|
533
|
+
await self.zep.memory.add(
|
|
534
|
+
user_id=user_id, session_id=user_id, messages=messages
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# Generate and stream the audio response
|
|
538
|
+
with self.client.audio.speech.with_streaming_response.create(
|
|
539
|
+
model="tts-1",
|
|
540
|
+
voice=voice,
|
|
541
|
+
input=full_response,
|
|
542
|
+
response_format=response_format,
|
|
543
|
+
) as response:
|
|
544
|
+
for chunk in response.iter_bytes(1024):
|
|
545
|
+
yield chunk
|
|
546
|
+
|
|
547
|
+
def handle_requires_action(self, data, run_id):
|
|
548
|
+
tool_outputs = []
|
|
549
|
+
|
|
550
|
+
for tool in data.required_action.submit_tool_outputs.tool_calls:
|
|
551
|
+
if tool.function.name in self.tool_handlers:
|
|
552
|
+
handler = self.tool_handlers[tool.function.name]
|
|
553
|
+
inputs = json.loads(tool.function.arguments)
|
|
554
|
+
output = handler(**inputs)
|
|
555
|
+
tool_outputs.append(
|
|
556
|
+
{"tool_call_id": tool.id, "output": output})
|
|
557
|
+
|
|
558
|
+
self.submit_tool_outputs(tool_outputs, run_id)
|
|
559
|
+
|
|
560
|
+
def submit_tool_outputs(self, tool_outputs, run_id):
|
|
561
|
+
with self.client.beta.threads.runs.submit_tool_outputs_stream(
|
|
562
|
+
thread_id=self.current_thread_id, run_id=run_id, tool_outputs=tool_outputs
|
|
563
|
+
) as stream:
|
|
564
|
+
for text in stream.text_deltas:
|
|
565
|
+
asyncio.create_task(self.accumulated_value_queue.put(text))
|
|
566
|
+
|
|
567
|
+
def add_tool(self, func: Callable):
|
|
568
|
+
sig = inspect.signature(func)
|
|
569
|
+
parameters = {"type": "object", "properties": {}, "required": []}
|
|
570
|
+
for name, param in sig.parameters.items():
|
|
571
|
+
parameters["properties"][name] = {
|
|
572
|
+
"type": "string", "description": "foo"}
|
|
573
|
+
if param.default == inspect.Parameter.empty:
|
|
574
|
+
parameters["required"].append(name)
|
|
575
|
+
tool_config = {
|
|
576
|
+
"type": "function",
|
|
577
|
+
"function": {
|
|
578
|
+
"name": func.__name__,
|
|
579
|
+
"description": func.__doc__ or "",
|
|
580
|
+
"parameters": parameters,
|
|
581
|
+
},
|
|
582
|
+
}
|
|
583
|
+
self.tools.append(tool_config)
|
|
584
|
+
self.tool_handlers[func.__name__] = func
|
|
585
|
+
return func
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
tool = AI.add_tool
|