jaf-py 2.4.1__py3-none-any.whl → 2.4.3__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.
- jaf/__init__.py +15 -0
- jaf/core/agent_tool.py +6 -4
- jaf/core/analytics.py +4 -3
- jaf/core/engine.py +401 -37
- jaf/core/state.py +156 -0
- jaf/core/tracing.py +114 -23
- jaf/core/types.py +113 -3
- jaf/memory/approval_storage.py +306 -0
- jaf/memory/providers/postgres.py +10 -4
- jaf/memory/types.py +1 -0
- jaf/memory/utils.py +1 -1
- jaf/providers/model.py +277 -17
- jaf/server/__init__.py +2 -0
- jaf/server/server.py +665 -22
- jaf/server/types.py +149 -4
- jaf/utils/__init__.py +50 -0
- jaf/utils/attachments.py +401 -0
- jaf/utils/document_processor.py +561 -0
- {jaf_py-2.4.1.dist-info → jaf_py-2.4.3.dist-info}/METADATA +10 -2
- {jaf_py-2.4.1.dist-info → jaf_py-2.4.3.dist-info}/RECORD +24 -19
- {jaf_py-2.4.1.dist-info → jaf_py-2.4.3.dist-info}/WHEEL +0 -0
- {jaf_py-2.4.1.dist-info → jaf_py-2.4.3.dist-info}/entry_points.txt +0 -0
- {jaf_py-2.4.1.dist-info → jaf_py-2.4.3.dist-info}/licenses/LICENSE +0 -0
- {jaf_py-2.4.1.dist-info → jaf_py-2.4.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Approval storage interface and implementations for Human-in-the-Loop (HITL) functionality.
|
|
3
|
+
|
|
4
|
+
This module provides persistent storage for tool approval decisions, enabling
|
|
5
|
+
the framework to maintain approval states across conversation sessions and
|
|
6
|
+
handle interruptions gracefully.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from typing import Dict, Any, Optional
|
|
11
|
+
import asyncio
|
|
12
|
+
|
|
13
|
+
from ..core.types import RunId, ApprovalValue
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ApprovalStorageResult:
|
|
17
|
+
"""Result wrapper for approval storage operations."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, success: bool, data: Any = None, error: Optional[str] = None):
|
|
20
|
+
self.success = success
|
|
21
|
+
self.data = data
|
|
22
|
+
self.error = error
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def success_result(cls, data: Any = None) -> 'ApprovalStorageResult':
|
|
26
|
+
"""Create a successful result."""
|
|
27
|
+
return cls(success=True, data=data)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def error_result(cls, error: str) -> 'ApprovalStorageResult':
|
|
31
|
+
"""Create an error result."""
|
|
32
|
+
return cls(success=False, error=error)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ApprovalStorage(ABC):
|
|
36
|
+
"""Abstract interface for approval storage implementations."""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
async def store_approval(
|
|
40
|
+
self,
|
|
41
|
+
run_id: RunId,
|
|
42
|
+
tool_call_id: str,
|
|
43
|
+
approval: ApprovalValue,
|
|
44
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
45
|
+
) -> ApprovalStorageResult:
|
|
46
|
+
"""Store an approval decision for a tool call."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
async def get_approval(
|
|
51
|
+
self,
|
|
52
|
+
run_id: RunId,
|
|
53
|
+
tool_call_id: str
|
|
54
|
+
) -> ApprovalStorageResult:
|
|
55
|
+
"""Retrieve approval for a specific tool call. Returns None if not found."""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def get_run_approvals(
|
|
60
|
+
self,
|
|
61
|
+
run_id: RunId
|
|
62
|
+
) -> ApprovalStorageResult:
|
|
63
|
+
"""Get all approvals for a run as a Dict[str, ApprovalValue]."""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
async def update_approval(
|
|
68
|
+
self,
|
|
69
|
+
run_id: RunId,
|
|
70
|
+
tool_call_id: str,
|
|
71
|
+
updates: Dict[str, Any]
|
|
72
|
+
) -> ApprovalStorageResult:
|
|
73
|
+
"""Update existing approval with additional context."""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
async def delete_approval(
|
|
78
|
+
self,
|
|
79
|
+
run_id: RunId,
|
|
80
|
+
tool_call_id: str
|
|
81
|
+
) -> ApprovalStorageResult:
|
|
82
|
+
"""Delete approval for a tool call. Returns success status."""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
async def clear_run_approvals(self, run_id: RunId) -> ApprovalStorageResult:
|
|
87
|
+
"""Clear all approvals for a run. Returns count of deleted approvals."""
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
async def get_stats(self) -> ApprovalStorageResult:
|
|
92
|
+
"""Get approval statistics."""
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
async def health_check(self) -> ApprovalStorageResult:
|
|
97
|
+
"""Health check for the approval storage."""
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
async def close(self) -> ApprovalStorageResult:
|
|
102
|
+
"""Close/cleanup the storage."""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class InMemoryApprovalStorage(ApprovalStorage):
|
|
107
|
+
"""In-memory implementation of ApprovalStorage for development and testing."""
|
|
108
|
+
|
|
109
|
+
def __init__(self):
|
|
110
|
+
self._approvals: Dict[str, Dict[str, ApprovalValue]] = {}
|
|
111
|
+
self._lock = asyncio.Lock()
|
|
112
|
+
|
|
113
|
+
def _get_run_key(self, run_id: RunId) -> str:
|
|
114
|
+
"""Generate a consistent key for a run."""
|
|
115
|
+
return f"run:{run_id}"
|
|
116
|
+
|
|
117
|
+
async def store_approval(
|
|
118
|
+
self,
|
|
119
|
+
run_id: RunId,
|
|
120
|
+
tool_call_id: str,
|
|
121
|
+
approval: ApprovalValue,
|
|
122
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
123
|
+
) -> ApprovalStorageResult:
|
|
124
|
+
"""Store an approval decision."""
|
|
125
|
+
try:
|
|
126
|
+
async with self._lock:
|
|
127
|
+
run_key = self._get_run_key(run_id)
|
|
128
|
+
|
|
129
|
+
if run_key not in self._approvals:
|
|
130
|
+
self._approvals[run_key] = {}
|
|
131
|
+
|
|
132
|
+
# Enhance approval with metadata if provided
|
|
133
|
+
enhanced_approval = approval
|
|
134
|
+
if metadata:
|
|
135
|
+
additional_context = {**(approval.additional_context or {}), **metadata}
|
|
136
|
+
enhanced_approval = ApprovalValue(
|
|
137
|
+
status=approval.status,
|
|
138
|
+
approved=approval.approved,
|
|
139
|
+
additional_context=additional_context
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
self._approvals[run_key][tool_call_id] = enhanced_approval
|
|
143
|
+
|
|
144
|
+
return ApprovalStorageResult.success_result()
|
|
145
|
+
except Exception as e:
|
|
146
|
+
return ApprovalStorageResult.error_result(f"Failed to store approval: {e}")
|
|
147
|
+
|
|
148
|
+
async def get_approval(
|
|
149
|
+
self,
|
|
150
|
+
run_id: RunId,
|
|
151
|
+
tool_call_id: str
|
|
152
|
+
) -> ApprovalStorageResult:
|
|
153
|
+
"""Retrieve approval for a specific tool call."""
|
|
154
|
+
try:
|
|
155
|
+
async with self._lock:
|
|
156
|
+
run_key = self._get_run_key(run_id)
|
|
157
|
+
run_approvals = self._approvals.get(run_key, {})
|
|
158
|
+
approval = run_approvals.get(tool_call_id)
|
|
159
|
+
|
|
160
|
+
return ApprovalStorageResult.success_result(approval)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
return ApprovalStorageResult.error_result(f"Failed to get approval: {e}")
|
|
163
|
+
|
|
164
|
+
async def get_run_approvals(self, run_id: RunId) -> ApprovalStorageResult:
|
|
165
|
+
"""Get all approvals for a run."""
|
|
166
|
+
try:
|
|
167
|
+
async with self._lock:
|
|
168
|
+
run_key = self._get_run_key(run_id)
|
|
169
|
+
run_approvals = self._approvals.get(run_key, {}).copy()
|
|
170
|
+
|
|
171
|
+
return ApprovalStorageResult.success_result(run_approvals)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
return ApprovalStorageResult.error_result(f"Failed to get run approvals: {e}")
|
|
174
|
+
|
|
175
|
+
async def update_approval(
|
|
176
|
+
self,
|
|
177
|
+
run_id: RunId,
|
|
178
|
+
tool_call_id: str,
|
|
179
|
+
updates: Dict[str, Any]
|
|
180
|
+
) -> ApprovalStorageResult:
|
|
181
|
+
"""Update existing approval."""
|
|
182
|
+
try:
|
|
183
|
+
async with self._lock:
|
|
184
|
+
run_key = self._get_run_key(run_id)
|
|
185
|
+
|
|
186
|
+
if run_key not in self._approvals or tool_call_id not in self._approvals[run_key]:
|
|
187
|
+
return ApprovalStorageResult.error_result(
|
|
188
|
+
f"Approval not found for tool call {tool_call_id} in run {run_id}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
existing = self._approvals[run_key][tool_call_id]
|
|
192
|
+
|
|
193
|
+
# Merge additional context
|
|
194
|
+
merged_context = {**(existing.additional_context or {}), **(updates.get('additional_context', {}))}
|
|
195
|
+
|
|
196
|
+
updated_approval = ApprovalValue(
|
|
197
|
+
status=updates.get('status', existing.status),
|
|
198
|
+
approved=updates.get('approved', existing.approved),
|
|
199
|
+
additional_context=merged_context if merged_context else existing.additional_context
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
self._approvals[run_key][tool_call_id] = updated_approval
|
|
203
|
+
|
|
204
|
+
return ApprovalStorageResult.success_result()
|
|
205
|
+
except Exception as e:
|
|
206
|
+
return ApprovalStorageResult.error_result(f"Failed to update approval: {e}")
|
|
207
|
+
|
|
208
|
+
async def delete_approval(
|
|
209
|
+
self,
|
|
210
|
+
run_id: RunId,
|
|
211
|
+
tool_call_id: str
|
|
212
|
+
) -> ApprovalStorageResult:
|
|
213
|
+
"""Delete approval for a tool call."""
|
|
214
|
+
try:
|
|
215
|
+
async with self._lock:
|
|
216
|
+
run_key = self._get_run_key(run_id)
|
|
217
|
+
|
|
218
|
+
if run_key not in self._approvals:
|
|
219
|
+
return ApprovalStorageResult.success_result(False)
|
|
220
|
+
|
|
221
|
+
deleted = self._approvals[run_key].pop(tool_call_id, None) is not None
|
|
222
|
+
|
|
223
|
+
# Clean up empty run maps
|
|
224
|
+
if not self._approvals[run_key]:
|
|
225
|
+
del self._approvals[run_key]
|
|
226
|
+
|
|
227
|
+
return ApprovalStorageResult.success_result(deleted)
|
|
228
|
+
except Exception as e:
|
|
229
|
+
return ApprovalStorageResult.error_result(f"Failed to delete approval: {e}")
|
|
230
|
+
|
|
231
|
+
async def clear_run_approvals(self, run_id: RunId) -> ApprovalStorageResult:
|
|
232
|
+
"""Clear all approvals for a run."""
|
|
233
|
+
try:
|
|
234
|
+
async with self._lock:
|
|
235
|
+
run_key = self._get_run_key(run_id)
|
|
236
|
+
|
|
237
|
+
if run_key not in self._approvals:
|
|
238
|
+
return ApprovalStorageResult.success_result(0)
|
|
239
|
+
|
|
240
|
+
count = len(self._approvals[run_key])
|
|
241
|
+
del self._approvals[run_key]
|
|
242
|
+
|
|
243
|
+
return ApprovalStorageResult.success_result(count)
|
|
244
|
+
except Exception as e:
|
|
245
|
+
return ApprovalStorageResult.error_result(f"Failed to clear run approvals: {e}")
|
|
246
|
+
|
|
247
|
+
async def get_stats(self) -> ApprovalStorageResult:
|
|
248
|
+
"""Get approval statistics."""
|
|
249
|
+
try:
|
|
250
|
+
async with self._lock:
|
|
251
|
+
total_approvals = 0
|
|
252
|
+
approved_count = 0
|
|
253
|
+
rejected_count = 0
|
|
254
|
+
runs_with_approvals = len(self._approvals)
|
|
255
|
+
|
|
256
|
+
for run_approvals in self._approvals.values():
|
|
257
|
+
for approval in run_approvals.values():
|
|
258
|
+
total_approvals += 1
|
|
259
|
+
if approval.approved:
|
|
260
|
+
approved_count += 1
|
|
261
|
+
else:
|
|
262
|
+
rejected_count += 1
|
|
263
|
+
|
|
264
|
+
stats = {
|
|
265
|
+
'total_approvals': total_approvals,
|
|
266
|
+
'approved_count': approved_count,
|
|
267
|
+
'rejected_count': rejected_count,
|
|
268
|
+
'runs_with_approvals': runs_with_approvals
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return ApprovalStorageResult.success_result(stats)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
return ApprovalStorageResult.error_result(f"Failed to get stats: {e}")
|
|
274
|
+
|
|
275
|
+
async def health_check(self) -> ApprovalStorageResult:
|
|
276
|
+
"""Health check for the storage."""
|
|
277
|
+
try:
|
|
278
|
+
# Simple operation to test functionality
|
|
279
|
+
await asyncio.sleep(0.001) # Minimal async operation
|
|
280
|
+
|
|
281
|
+
health_data = {
|
|
282
|
+
'healthy': True,
|
|
283
|
+
'latency_ms': 1.0 # Approximate for in-memory
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return ApprovalStorageResult.success_result(health_data)
|
|
287
|
+
except Exception as e:
|
|
288
|
+
health_data = {
|
|
289
|
+
'healthy': False,
|
|
290
|
+
'error': str(e)
|
|
291
|
+
}
|
|
292
|
+
return ApprovalStorageResult.success_result(health_data)
|
|
293
|
+
|
|
294
|
+
async def close(self) -> ApprovalStorageResult:
|
|
295
|
+
"""Close/cleanup the storage."""
|
|
296
|
+
try:
|
|
297
|
+
async with self._lock:
|
|
298
|
+
self._approvals.clear()
|
|
299
|
+
return ApprovalStorageResult.success_result()
|
|
300
|
+
except Exception as e:
|
|
301
|
+
return ApprovalStorageResult.error_result(f"Failed to close storage: {e}")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def create_in_memory_approval_storage() -> InMemoryApprovalStorage:
|
|
305
|
+
"""Create an in-memory approval storage instance."""
|
|
306
|
+
return InMemoryApprovalStorage()
|
jaf/memory/providers/postgres.py
CHANGED
|
@@ -261,16 +261,22 @@ async def create_postgres_provider(config: PostgresConfig) -> Result[PostgresPro
|
|
|
261
261
|
# but we can continue and hope the database already exists.
|
|
262
262
|
print(f"Could not ensure database exists: {e}")
|
|
263
263
|
|
|
264
|
-
# Now connect to the target database
|
|
264
|
+
# Now connect to the target database using connection pool with max_connections
|
|
265
265
|
if config.connection_string:
|
|
266
|
-
client = await asyncpg.
|
|
266
|
+
client = await asyncpg.create_pool(
|
|
267
|
+
dsn=config.connection_string,
|
|
268
|
+
min_size=1,
|
|
269
|
+
max_size=config.max_connections
|
|
270
|
+
)
|
|
267
271
|
else:
|
|
268
|
-
client = await asyncpg.
|
|
272
|
+
client = await asyncpg.create_pool(
|
|
269
273
|
host=config.host,
|
|
270
274
|
port=config.port,
|
|
271
275
|
user=config.username,
|
|
272
276
|
password=config.password,
|
|
273
|
-
database=config.database
|
|
277
|
+
database=config.database,
|
|
278
|
+
min_size=1,
|
|
279
|
+
max_size=config.max_connections
|
|
274
280
|
)
|
|
275
281
|
|
|
276
282
|
table_name = config.table_name or "conversations"
|
jaf/memory/types.py
CHANGED
jaf/memory/utils.py
CHANGED
|
@@ -9,7 +9,7 @@ import json
|
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from typing import Any, Dict, List, Optional
|
|
11
11
|
|
|
12
|
-
from ..core.types import Message, ToolCall, ToolCallFunction
|
|
12
|
+
from ..core.types import Message, ToolCall, ToolCallFunction, get_text_content
|
|
13
13
|
from .types import ConversationMemory
|
|
14
14
|
|
|
15
15
|
|