solana-agent 31.0.0__py3-none-any.whl → 31.1.1__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.
- solana_agent/client/solana_agent.py +4 -0
- solana_agent/domains/agent.py +7 -1
- solana_agent/factories/agent_factory.py +37 -6
- solana_agent/interfaces/providers/memory.py +12 -0
- solana_agent/interfaces/services/query.py +2 -0
- solana_agent/repositories/memory.py +150 -90
- solana_agent/services/agent.py +33 -1
- solana_agent/services/query.py +473 -190
- solana_agent/services/routing.py +19 -13
- {solana_agent-31.0.0.dist-info → solana_agent-31.1.1.dist-info}/METADATA +32 -27
- {solana_agent-31.0.0.dist-info → solana_agent-31.1.1.dist-info}/RECORD +14 -14
- {solana_agent-31.0.0.dist-info → solana_agent-31.1.1.dist-info}/LICENSE +0 -0
- {solana_agent-31.0.0.dist-info → solana_agent-31.1.1.dist-info}/WHEEL +0 -0
- {solana_agent-31.0.0.dist-info → solana_agent-31.1.1.dist-info}/entry_points.txt +0 -0
@@ -49,6 +49,8 @@ class SolanaAgent(SolanaAgentInterface):
|
|
49
49
|
user_id: str,
|
50
50
|
message: Union[str, bytes],
|
51
51
|
prompt: Optional[str] = None,
|
52
|
+
capture_schema: Optional[Dict[str, Any]] = None,
|
53
|
+
capture_name: Optional[str] = None,
|
52
54
|
output_format: Literal["text", "audio"] = "text",
|
53
55
|
audio_voice: Literal[
|
54
56
|
"alloy",
|
@@ -103,6 +105,8 @@ class SolanaAgent(SolanaAgentInterface):
|
|
103
105
|
prompt=prompt,
|
104
106
|
router=router,
|
105
107
|
output_model=output_model,
|
108
|
+
capture_schema=capture_schema,
|
109
|
+
capture_name=capture_name,
|
106
110
|
):
|
107
111
|
yield chunk
|
108
112
|
|
solana_agent/domains/agent.py
CHANGED
@@ -5,7 +5,7 @@ This module defines the core domain models for representing
|
|
5
5
|
AI agents, human agents, and business mission/values.
|
6
6
|
"""
|
7
7
|
|
8
|
-
from typing import List, Dict
|
8
|
+
from typing import List, Dict, Optional, Any
|
9
9
|
from pydantic import BaseModel, Field, field_validator
|
10
10
|
|
11
11
|
|
@@ -53,6 +53,12 @@ class AIAgent(BaseModel):
|
|
53
53
|
name: str = Field(..., description="Unique agent identifier name")
|
54
54
|
instructions: str = Field(..., description="Base instructions for the agent")
|
55
55
|
specialization: str = Field(..., description="Agent's specialized domain")
|
56
|
+
capture_name: Optional[str] = Field(
|
57
|
+
default=None, description="Optional capture name for structured data"
|
58
|
+
)
|
59
|
+
capture_schema: Optional[Dict[str, Any]] = Field(
|
60
|
+
default=None, description="Optional JSON schema for structured capture"
|
61
|
+
)
|
56
62
|
|
57
63
|
@field_validator("name", "specialization")
|
58
64
|
@classmethod
|
@@ -133,21 +133,38 @@ class SolanaAgentFactory:
|
|
133
133
|
voice=org_config.get("voice", ""),
|
134
134
|
)
|
135
135
|
|
136
|
+
# Build capture modes from agent config if provided
|
137
|
+
capture_modes: Dict[str, str] = {}
|
138
|
+
for agent in config.get("agents", []):
|
139
|
+
mode = agent.get("capture_mode")
|
140
|
+
if mode in {"once", "multiple"} and agent.get("name"):
|
141
|
+
capture_modes[agent["name"]] = mode
|
142
|
+
|
136
143
|
# Create repositories
|
137
144
|
memory_provider = None
|
138
145
|
|
139
146
|
if "zep" in config and "mongo" in config:
|
140
|
-
|
141
|
-
mongo_adapter
|
142
|
-
|
147
|
+
mem_kwargs: Dict[str, Any] = {
|
148
|
+
"mongo_adapter": db_adapter,
|
149
|
+
"zep_api_key": config["zep"].get("api_key"),
|
150
|
+
}
|
151
|
+
if capture_modes: # pragma: no cover
|
152
|
+
mem_kwargs["capture_modes"] = capture_modes
|
153
|
+
memory_provider = MemoryRepository(**mem_kwargs)
|
143
154
|
|
144
155
|
if "mongo" in config and "zep" not in config:
|
145
|
-
|
156
|
+
mem_kwargs = {"mongo_adapter": db_adapter}
|
157
|
+
if capture_modes:
|
158
|
+
mem_kwargs["capture_modes"] = capture_modes
|
159
|
+
memory_provider = MemoryRepository(**mem_kwargs)
|
146
160
|
|
147
161
|
if "zep" in config and "mongo" not in config:
|
148
162
|
if "api_key" not in config["zep"]:
|
149
163
|
raise ValueError("Zep API key is required.")
|
150
|
-
|
164
|
+
mem_kwargs = {"zep_api_key": config["zep"].get("api_key")}
|
165
|
+
if capture_modes: # pragma: no cover
|
166
|
+
mem_kwargs["capture_modes"] = capture_modes
|
167
|
+
memory_provider = MemoryRepository(**mem_kwargs)
|
151
168
|
|
152
169
|
guardrail_config = config.get("guardrails", {})
|
153
170
|
input_guardrails: List[InputGuardrail] = SolanaAgentFactory._create_guardrails(
|
@@ -169,9 +186,16 @@ class SolanaAgentFactory:
|
|
169
186
|
)
|
170
187
|
|
171
188
|
# Create routing service
|
189
|
+
# Optional routing model override (use small, cheap model by default in service)
|
190
|
+
routing_model = (
|
191
|
+
config.get("openai", {}).get("routing_model")
|
192
|
+
if isinstance(config.get("openai"), dict)
|
193
|
+
else None
|
194
|
+
)
|
172
195
|
routing_service = RoutingService(
|
173
196
|
llm_provider=llm_adapter,
|
174
197
|
agent_service=agent_service,
|
198
|
+
model=routing_model,
|
175
199
|
)
|
176
200
|
|
177
201
|
# Debug the agent service tool registry
|
@@ -191,11 +215,18 @@ class SolanaAgentFactory:
|
|
191
215
|
loaded_plugins = 0
|
192
216
|
|
193
217
|
# Register predefined agents
|
194
|
-
for agent_config in config.get("agents", []):
|
218
|
+
for agent_config in config.get("agents", []): # pragma: no cover
|
219
|
+
extra_kwargs = {}
|
220
|
+
if "capture_name" in agent_config:
|
221
|
+
extra_kwargs["capture_name"] = agent_config.get("capture_name")
|
222
|
+
if "capture_schema" in agent_config:
|
223
|
+
extra_kwargs["capture_schema"] = agent_config.get("capture_schema")
|
224
|
+
|
195
225
|
agent_service.register_ai_agent(
|
196
226
|
name=agent_config["name"],
|
197
227
|
instructions=agent_config["instructions"],
|
198
228
|
specialization=agent_config["specialization"],
|
229
|
+
**extra_kwargs,
|
199
230
|
)
|
200
231
|
|
201
232
|
# Register tools for this agent
|
@@ -36,3 +36,15 @@ class MemoryProvider(ABC):
|
|
36
36
|
def count_documents(self, collection: str, query: Dict) -> int:
|
37
37
|
"""Count documents matching query."""
|
38
38
|
pass
|
39
|
+
|
40
|
+
@abstractmethod
|
41
|
+
async def save_capture(
|
42
|
+
self,
|
43
|
+
user_id: str,
|
44
|
+
capture_name: str,
|
45
|
+
agent_name: Optional[str],
|
46
|
+
data: Dict[str, Any],
|
47
|
+
schema: Optional[Dict[str, Any]] = None,
|
48
|
+
) -> Optional[str]:
|
49
|
+
"""Persist a structured capture for a user and return its ID if available."""
|
50
|
+
pass
|
@@ -38,6 +38,8 @@ class QueryService(ABC):
|
|
38
38
|
router: Optional[RoutingInterface] = None,
|
39
39
|
images: Optional[List[Union[str, bytes]]] = None,
|
40
40
|
output_model: Optional[Type[BaseModel]] = None,
|
41
|
+
capture_schema: Optional[Dict[str, Any]] = None,
|
42
|
+
capture_name: Optional[str] = None,
|
41
43
|
) -> AsyncGenerator[Union[str, bytes, BaseModel], None]:
|
42
44
|
"""Process the user request and generate a response."""
|
43
45
|
pass
|
@@ -1,13 +1,14 @@
|
|
1
|
-
import logging
|
2
|
-
from
|
3
|
-
from typing import List, Dict, Any, Optional, Tuple
|
1
|
+
import logging
|
2
|
+
from typing import List, Dict, Optional, Tuple, Any
|
4
3
|
from datetime import datetime, timezone
|
4
|
+
from copy import deepcopy
|
5
|
+
|
5
6
|
from zep_cloud.client import AsyncZep as AsyncZepCloud
|
6
7
|
from zep_cloud.types import Message
|
8
|
+
|
7
9
|
from solana_agent.interfaces.providers.memory import MemoryProvider
|
8
10
|
from solana_agent.adapters.mongodb_adapter import MongoDBAdapter
|
9
11
|
|
10
|
-
# Setup logger for this module
|
11
12
|
logger = logging.getLogger(__name__)
|
12
13
|
|
13
14
|
|
@@ -18,159 +19,157 @@ class MemoryRepository(MemoryProvider):
|
|
18
19
|
self,
|
19
20
|
mongo_adapter: Optional[MongoDBAdapter] = None,
|
20
21
|
zep_api_key: Optional[str] = None,
|
22
|
+
capture_modes: Optional[Dict[str, str]] = None,
|
21
23
|
):
|
22
|
-
|
24
|
+
self.capture_modes: Dict[str, str] = capture_modes or {}
|
25
|
+
|
26
|
+
# Mongo setup
|
23
27
|
if not mongo_adapter:
|
24
28
|
self.mongo = None
|
25
29
|
self.collection = None
|
30
|
+
self.captures_collection = "captures"
|
26
31
|
else:
|
27
|
-
# Initialize MongoDB
|
28
32
|
self.mongo = mongo_adapter
|
29
33
|
self.collection = "conversations"
|
30
|
-
|
31
34
|
try:
|
32
|
-
# Ensure MongoDB collection and indexes
|
33
35
|
self.mongo.create_collection(self.collection)
|
34
36
|
self.mongo.create_index(self.collection, [("user_id", 1)])
|
35
37
|
self.mongo.create_index(self.collection, [("timestamp", 1)])
|
36
38
|
except Exception as e:
|
37
|
-
logger.error(f"Error initializing MongoDB: {e}")
|
39
|
+
logger.error(f"Error initializing MongoDB: {e}")
|
40
|
+
|
41
|
+
try:
|
42
|
+
self.captures_collection = "captures"
|
43
|
+
self.mongo.create_collection(self.captures_collection)
|
44
|
+
# Basic indexes
|
45
|
+
self.mongo.create_index(self.captures_collection, [("user_id", 1)])
|
46
|
+
self.mongo.create_index(self.captures_collection, [("capture_name", 1)])
|
47
|
+
self.mongo.create_index(self.captures_collection, [("agent_name", 1)])
|
48
|
+
self.mongo.create_index(self.captures_collection, [("timestamp", 1)])
|
49
|
+
# Unique only when mode == 'once'
|
50
|
+
try:
|
51
|
+
self.mongo.create_index(
|
52
|
+
self.captures_collection,
|
53
|
+
[("user_id", 1), ("agent_name", 1), ("capture_name", 1)],
|
54
|
+
unique=True,
|
55
|
+
partialFilterExpression={"mode": "once"},
|
56
|
+
)
|
57
|
+
except Exception as e:
|
58
|
+
logger.error(
|
59
|
+
f"Error creating partial unique index for captures: {e}"
|
60
|
+
)
|
61
|
+
except Exception as e:
|
62
|
+
logger.error(f"Error initializing MongoDB captures collection: {e}")
|
63
|
+
self.captures_collection = "captures"
|
38
64
|
|
39
|
-
|
40
|
-
|
41
|
-
if zep_api_key:
|
42
|
-
self.zep = AsyncZepCloud(api_key=zep_api_key)
|
65
|
+
# Zep setup
|
66
|
+
self.zep = AsyncZepCloud(api_key=zep_api_key) if zep_api_key else None
|
43
67
|
|
44
68
|
async def store(self, user_id: str, messages: List[Dict[str, Any]]) -> None:
|
45
|
-
|
46
|
-
if not user_id or user_id == "" or not isinstance(user_id, str):
|
69
|
+
if not user_id or not isinstance(user_id, str):
|
47
70
|
raise ValueError("User ID cannot be None or empty")
|
48
71
|
if not messages or not isinstance(messages, list):
|
49
72
|
raise ValueError("Messages must be a non-empty list")
|
50
73
|
if not all(
|
51
|
-
isinstance(
|
52
|
-
for msg in messages
|
74
|
+
isinstance(m, dict) and "role" in m and "content" in m for m in messages
|
53
75
|
):
|
54
76
|
raise ValueError(
|
55
77
|
"All messages must be dictionaries with 'role' and 'content' keys"
|
56
78
|
)
|
57
|
-
for
|
58
|
-
if
|
79
|
+
for m in messages:
|
80
|
+
if m["role"] not in ["user", "assistant"]:
|
59
81
|
raise ValueError(
|
60
|
-
|
82
|
+
"Invalid role in message. Only 'user' and 'assistant' are accepted."
|
61
83
|
)
|
62
84
|
|
63
|
-
#
|
85
|
+
# Persist last user/assistant pair to Mongo
|
64
86
|
if self.mongo and len(messages) >= 2:
|
65
87
|
try:
|
66
|
-
# Get last user and assistant messages
|
67
88
|
user_msg = None
|
68
89
|
assistant_msg = None
|
69
|
-
for
|
70
|
-
if
|
71
|
-
user_msg =
|
72
|
-
elif
|
73
|
-
assistant_msg =
|
90
|
+
for m in reversed(messages):
|
91
|
+
if m.get("role") == "user" and not user_msg:
|
92
|
+
user_msg = m.get("content")
|
93
|
+
elif m.get("role") == "assistant" and not assistant_msg:
|
94
|
+
assistant_msg = m.get("content")
|
74
95
|
if user_msg and assistant_msg:
|
75
96
|
break
|
76
|
-
|
77
97
|
if user_msg and assistant_msg:
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
98
|
+
self.mongo.insert_one(
|
99
|
+
self.collection,
|
100
|
+
{
|
101
|
+
"user_id": user_id,
|
102
|
+
"user_message": user_msg,
|
103
|
+
"assistant_message": assistant_msg,
|
104
|
+
"timestamp": datetime.now(timezone.utc),
|
105
|
+
},
|
106
|
+
)
|
86
107
|
except Exception as e:
|
87
|
-
logger.error(f"MongoDB storage error: {e}")
|
108
|
+
logger.error(f"MongoDB storage error: {e}")
|
88
109
|
|
89
|
-
#
|
110
|
+
# Zep
|
90
111
|
if not self.zep:
|
91
112
|
return
|
92
113
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
role=role_type,
|
103
|
-
)
|
104
|
-
zep_messages.append(zep_msg)
|
114
|
+
zep_messages: List[Message] = []
|
115
|
+
for m in messages:
|
116
|
+
content = (
|
117
|
+
self._truncate(deepcopy(m.get("content"))) if "content" in m else None
|
118
|
+
)
|
119
|
+
if content is None: # pragma: no cover
|
120
|
+
continue
|
121
|
+
role_type = "user" if m.get("role") == "user" else "assistant"
|
122
|
+
zep_messages.append(Message(content=content, role=role_type))
|
105
123
|
|
106
|
-
# Add messages to Zep memory
|
107
124
|
if zep_messages:
|
108
125
|
try:
|
109
126
|
await self.zep.thread.add_messages(
|
110
|
-
thread_id=user_id,
|
111
|
-
messages=zep_messages,
|
127
|
+
thread_id=user_id, messages=zep_messages
|
112
128
|
)
|
113
129
|
except Exception:
|
114
130
|
try:
|
115
131
|
try:
|
116
132
|
await self.zep.user.add(user_id=user_id)
|
117
133
|
except Exception as e:
|
118
|
-
logger.error(
|
119
|
-
f"Zep user addition error: {e}"
|
120
|
-
) # Use logger.error
|
121
|
-
|
134
|
+
logger.error(f"Zep user addition error: {e}")
|
122
135
|
try:
|
123
|
-
await self.zep.thread.create(
|
124
|
-
thread_id=user_id,
|
125
|
-
user_id=user_id,
|
126
|
-
)
|
136
|
+
await self.zep.thread.create(thread_id=user_id, user_id=user_id)
|
127
137
|
except Exception as e:
|
128
|
-
logger.error(
|
129
|
-
f"Zep thread creation error: {e}"
|
130
|
-
) # Use logger.error
|
138
|
+
logger.error(f"Zep thread creation error: {e}")
|
131
139
|
await self.zep.thread.add_messages(
|
132
|
-
thread_id=user_id,
|
133
|
-
messages=zep_messages,
|
140
|
+
thread_id=user_id, messages=zep_messages
|
134
141
|
)
|
135
142
|
except Exception as e:
|
136
|
-
logger.error(f"Zep memory addition error: {e}")
|
137
|
-
return
|
143
|
+
logger.error(f"Zep memory addition error: {e}")
|
138
144
|
|
139
145
|
async def retrieve(self, user_id: str) -> str:
|
140
|
-
"""Retrieve memory context from Zep."""
|
141
146
|
try:
|
142
147
|
memories = ""
|
143
148
|
if self.zep:
|
144
149
|
memory = await self.zep.thread.get_user_context(thread_id=user_id)
|
145
150
|
if memory and memory.context:
|
146
151
|
memories = memory.context
|
147
|
-
|
148
152
|
return memories
|
149
|
-
|
150
153
|
except Exception as e:
|
151
|
-
logger.error(f"Error retrieving memories: {e}")
|
154
|
+
logger.error(f"Error retrieving memories: {e}")
|
152
155
|
return ""
|
153
156
|
|
154
157
|
async def delete(self, user_id: str) -> None:
|
155
|
-
"""Delete memory from both systems."""
|
156
158
|
if self.mongo:
|
157
159
|
try:
|
158
160
|
self.mongo.delete_all(self.collection, {"user_id": user_id})
|
159
161
|
except Exception as e:
|
160
|
-
logger.error(f"MongoDB deletion error: {e}")
|
161
|
-
|
162
|
+
logger.error(f"MongoDB deletion error: {e}")
|
162
163
|
if not self.zep:
|
163
164
|
return
|
164
|
-
|
165
165
|
try:
|
166
166
|
await self.zep.thread.delete(thread_id=user_id)
|
167
167
|
except Exception as e:
|
168
|
-
logger.error(f"Zep memory deletion error: {e}")
|
169
|
-
|
168
|
+
logger.error(f"Zep memory deletion error: {e}")
|
170
169
|
try:
|
171
170
|
await self.zep.user.delete(user_id=user_id)
|
172
171
|
except Exception as e:
|
173
|
-
logger.error(f"Zep user deletion error: {e}")
|
172
|
+
logger.error(f"Zep user deletion error: {e}")
|
174
173
|
|
175
174
|
def find(
|
176
175
|
self,
|
@@ -180,37 +179,98 @@ class MemoryRepository(MemoryProvider):
|
|
180
179
|
limit: int = 0,
|
181
180
|
skip: int = 0,
|
182
181
|
) -> List[Dict]: # pragma: no cover
|
183
|
-
"""Find documents in MongoDB."""
|
184
182
|
if not self.mongo:
|
185
183
|
return []
|
186
|
-
|
187
184
|
try:
|
188
185
|
return self.mongo.find(collection, query, sort=sort, limit=limit, skip=skip)
|
189
186
|
except Exception as e:
|
190
|
-
logger.error(f"MongoDB find error: {e}")
|
187
|
+
logger.error(f"MongoDB find error: {e}")
|
191
188
|
return []
|
192
189
|
|
193
190
|
def count_documents(self, collection: str, query: Dict) -> int:
|
194
|
-
"""Count documents in MongoDB."""
|
195
191
|
if not self.mongo:
|
196
192
|
return 0
|
197
193
|
return self.mongo.count_documents(collection, query)
|
198
194
|
|
199
195
|
def _truncate(self, text: str, limit: int = 2500) -> str:
|
200
|
-
"""Truncate text to be within limits."""
|
201
196
|
if text is None:
|
202
197
|
raise AttributeError("Cannot truncate None text")
|
203
|
-
|
204
198
|
if not text:
|
205
199
|
return ""
|
206
|
-
|
207
200
|
if len(text) <= limit:
|
208
201
|
return text
|
209
|
-
|
210
|
-
# Try to truncate at last period before limit
|
211
202
|
last_period = text.rfind(".", 0, limit)
|
212
203
|
if last_period > 0:
|
213
204
|
return text[: last_period + 1]
|
214
|
-
|
215
|
-
# If no period found, truncate at limit and add ellipsis
|
216
205
|
return text[: limit - 3] + "..."
|
206
|
+
|
207
|
+
async def save_capture(
|
208
|
+
self,
|
209
|
+
user_id: str,
|
210
|
+
capture_name: str,
|
211
|
+
agent_name: Optional[str],
|
212
|
+
data: Dict[str, Any],
|
213
|
+
schema: Optional[Dict[str, Any]] = None,
|
214
|
+
) -> Optional[str]:
|
215
|
+
if not self.mongo: # pragma: no cover
|
216
|
+
logger.warning("MongoDB not configured; cannot save capture.")
|
217
|
+
return None
|
218
|
+
if not user_id or not isinstance(user_id, str):
|
219
|
+
raise ValueError("user_id must be a non-empty string")
|
220
|
+
if not capture_name or not isinstance(capture_name, str):
|
221
|
+
raise ValueError("capture_name must be a non-empty string")
|
222
|
+
if not isinstance(data, dict):
|
223
|
+
raise ValueError("data must be a dictionary")
|
224
|
+
|
225
|
+
try:
|
226
|
+
mode = self.capture_modes.get(agent_name, "once") if agent_name else "once"
|
227
|
+
now = datetime.now(timezone.utc)
|
228
|
+
if mode == "multiple":
|
229
|
+
doc = {
|
230
|
+
"user_id": user_id,
|
231
|
+
"agent_name": agent_name,
|
232
|
+
"capture_name": capture_name,
|
233
|
+
"data": data or {},
|
234
|
+
"schema": schema or {},
|
235
|
+
"mode": "multiple",
|
236
|
+
"timestamp": now,
|
237
|
+
"created_at": now,
|
238
|
+
}
|
239
|
+
return self.mongo.insert_one(self.captures_collection, doc)
|
240
|
+
else:
|
241
|
+
key = {
|
242
|
+
"user_id": user_id,
|
243
|
+
"agent_name": agent_name,
|
244
|
+
"capture_name": capture_name,
|
245
|
+
}
|
246
|
+
existing = self.mongo.find_one(self.captures_collection, key)
|
247
|
+
merged_data: Dict[str, Any] = {}
|
248
|
+
if existing and isinstance(existing.get("data"), dict):
|
249
|
+
merged_data.update(existing.get("data", {}))
|
250
|
+
merged_data.update(data or {})
|
251
|
+
update_doc = {
|
252
|
+
"$set": {
|
253
|
+
"user_id": user_id,
|
254
|
+
"agent_name": agent_name,
|
255
|
+
"capture_name": capture_name,
|
256
|
+
"data": merged_data,
|
257
|
+
"schema": (
|
258
|
+
schema
|
259
|
+
if schema is not None
|
260
|
+
else existing.get("schema")
|
261
|
+
if existing
|
262
|
+
else {}
|
263
|
+
),
|
264
|
+
"mode": "once",
|
265
|
+
"timestamp": now,
|
266
|
+
},
|
267
|
+
"$setOnInsert": {"created_at": now},
|
268
|
+
}
|
269
|
+
self.mongo.update_one(
|
270
|
+
self.captures_collection, key, update_doc, upsert=True
|
271
|
+
)
|
272
|
+
doc = self.mongo.find_one(self.captures_collection, key)
|
273
|
+
return str(doc.get("_id")) if doc and doc.get("_id") else None
|
274
|
+
except Exception as e: # pragma: no cover
|
275
|
+
logger.error(f"MongoDB save_capture error: {e}")
|
276
|
+
return None
|
solana_agent/services/agent.py
CHANGED
@@ -73,6 +73,8 @@ class AgentService(AgentServiceInterface):
|
|
73
73
|
name: str,
|
74
74
|
instructions: str,
|
75
75
|
specialization: str,
|
76
|
+
capture_name: Optional[str] = None,
|
77
|
+
capture_schema: Optional[Dict[str, Any]] = None,
|
76
78
|
) -> None:
|
77
79
|
"""Register an AI agent with its specialization.
|
78
80
|
|
@@ -85,6 +87,8 @@ class AgentService(AgentServiceInterface):
|
|
85
87
|
name=name,
|
86
88
|
instructions=instructions,
|
87
89
|
specialization=specialization,
|
90
|
+
capture_name=capture_name,
|
91
|
+
capture_schema=capture_schema,
|
88
92
|
)
|
89
93
|
self.agents.append(agent)
|
90
94
|
logger.info(f"Registered AI agent: {name}")
|
@@ -98,7 +102,6 @@ class AgentService(AgentServiceInterface):
|
|
98
102
|
Returns:
|
99
103
|
System prompt
|
100
104
|
"""
|
101
|
-
|
102
105
|
# Get agent by name
|
103
106
|
agent = next((a for a in self.agents if a.name == agent_name), None)
|
104
107
|
|
@@ -130,8 +133,37 @@ class AgentService(AgentServiceInterface):
|
|
130
133
|
)
|
131
134
|
system_prompt += f"\n\nBUSINESS GOALS:\n{goals_text}"
|
132
135
|
|
136
|
+
# Add capture guidance if this agent has a capture schema
|
137
|
+
if getattr(agent, "capture_schema", None) and getattr(
|
138
|
+
agent, "capture_name", None
|
139
|
+
): # pragma: no cover
|
140
|
+
system_prompt += (
|
141
|
+
"\n\nSTRUCTURED DATA CAPTURE:\n"
|
142
|
+
f"You must collect the following fields for the form '{agent.capture_name}'. "
|
143
|
+
"Ask concise follow-up questions to fill any missing required fields one at a time. "
|
144
|
+
"Confirm values when ambiguous, and summarize the captured data before finalizing.\n\n"
|
145
|
+
"JSON Schema (authoritative definition of the fields):\n"
|
146
|
+
f"{agent.capture_schema}\n\n"
|
147
|
+
"Rules:\n"
|
148
|
+
"- Never invent values—ask the user.\n"
|
149
|
+
"- Validate types (emails look like emails, numbers are numbers, booleans are yes/no).\n"
|
150
|
+
"- If the user declines to provide a required value, note it clearly.\n"
|
151
|
+
"- When all required fields are provided, acknowledge completion.\n"
|
152
|
+
)
|
153
|
+
|
133
154
|
return system_prompt
|
134
155
|
|
156
|
+
def get_agent_capture(
|
157
|
+
self, agent_name: str
|
158
|
+
) -> Optional[Dict[str, Any]]: # pragma: no cover
|
159
|
+
"""Return capture metadata for the agent, if any."""
|
160
|
+
agent = next((a for a in self.agents if a.name == agent_name), None)
|
161
|
+
if not agent:
|
162
|
+
return None
|
163
|
+
if agent.capture_name and agent.capture_schema:
|
164
|
+
return {"name": agent.capture_name, "schema": agent.capture_schema}
|
165
|
+
return None
|
166
|
+
|
135
167
|
def get_all_ai_agents(self) -> Dict[str, AIAgent]:
|
136
168
|
"""Get all registered AI agents.
|
137
169
|
|