jaf-py 2.3.0__py3-none-any.whl → 2.4.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.
- jaf/core/engine.py +111 -2
- jaf/core/tools.py +4 -6
- jaf/core/tracing.py +0 -2
- jaf/core/types.py +44 -1
- jaf/providers/model.py +165 -2
- {jaf_py-2.3.0.dist-info → jaf_py-2.4.1.dist-info}/METADATA +119 -119
- {jaf_py-2.3.0.dist-info → jaf_py-2.4.1.dist-info}/RECORD +11 -11
- {jaf_py-2.3.0.dist-info → jaf_py-2.4.1.dist-info}/WHEEL +0 -0
- {jaf_py-2.3.0.dist-info → jaf_py-2.4.1.dist-info}/entry_points.txt +0 -0
- {jaf_py-2.3.0.dist-info → jaf_py-2.4.1.dist-info}/licenses/LICENSE +0 -0
- {jaf_py-2.3.0.dist-info → jaf_py-2.4.1.dist-info}/top_level.txt +0 -0
jaf/core/engine.py
CHANGED
|
@@ -36,6 +36,8 @@ from .types import (
|
|
|
36
36
|
LLMCallEndEventData,
|
|
37
37
|
LLMCallStartEvent,
|
|
38
38
|
LLMCallStartEventData,
|
|
39
|
+
AssistantMessageEvent,
|
|
40
|
+
AssistantMessageEventData,
|
|
39
41
|
MaxTurnsExceeded,
|
|
40
42
|
Message,
|
|
41
43
|
ModelBehaviorError,
|
|
@@ -300,8 +302,115 @@ async def _run_internal(
|
|
|
300
302
|
messages=state.messages
|
|
301
303
|
))))
|
|
302
304
|
|
|
303
|
-
# Get completion from model provider
|
|
304
|
-
llm_response
|
|
305
|
+
# Get completion from model provider, prefer streaming if available
|
|
306
|
+
llm_response: Dict[str, Any]
|
|
307
|
+
assistant_event_streamed = False
|
|
308
|
+
|
|
309
|
+
get_stream = getattr(config.model_provider, "get_completion_stream", None)
|
|
310
|
+
if callable(get_stream):
|
|
311
|
+
try:
|
|
312
|
+
aggregated_text = ""
|
|
313
|
+
# Working array of partial tool calls
|
|
314
|
+
partial_tool_calls: List[Dict[str, Any]] = []
|
|
315
|
+
|
|
316
|
+
async for chunk in get_stream(state, current_agent, config): # type: ignore[arg-type]
|
|
317
|
+
# Text deltas
|
|
318
|
+
delta_text = getattr(chunk, "delta", None)
|
|
319
|
+
if delta_text:
|
|
320
|
+
aggregated_text += delta_text
|
|
321
|
+
|
|
322
|
+
# Tool call deltas
|
|
323
|
+
tcd = getattr(chunk, "tool_call_delta", None)
|
|
324
|
+
if tcd is not None:
|
|
325
|
+
idx = getattr(tcd, "index", 0) or 0
|
|
326
|
+
# Ensure slot exists
|
|
327
|
+
while len(partial_tool_calls) <= idx:
|
|
328
|
+
partial_tool_calls.append({
|
|
329
|
+
"id": None,
|
|
330
|
+
"type": "function",
|
|
331
|
+
"function": {"name": None, "arguments": ""}
|
|
332
|
+
})
|
|
333
|
+
target = partial_tool_calls[idx]
|
|
334
|
+
# id
|
|
335
|
+
tc_id = getattr(tcd, "id", None)
|
|
336
|
+
if tc_id:
|
|
337
|
+
target["id"] = tc_id
|
|
338
|
+
# function fields
|
|
339
|
+
fn = getattr(tcd, "function", None)
|
|
340
|
+
if fn is not None:
|
|
341
|
+
fn_name = getattr(fn, "name", None)
|
|
342
|
+
if fn_name:
|
|
343
|
+
target["function"]["name"] = fn_name
|
|
344
|
+
args_delta = getattr(fn, "arguments_delta", None)
|
|
345
|
+
if args_delta:
|
|
346
|
+
target["function"]["arguments"] += args_delta
|
|
347
|
+
|
|
348
|
+
# Emit partial assistant message when something changed
|
|
349
|
+
if delta_text or tcd is not None:
|
|
350
|
+
assistant_event_streamed = True
|
|
351
|
+
# Normalize tool_calls for message
|
|
352
|
+
message_tool_calls = None
|
|
353
|
+
if len(partial_tool_calls) > 0:
|
|
354
|
+
message_tool_calls = []
|
|
355
|
+
for i, tc in enumerate(partial_tool_calls):
|
|
356
|
+
message_tool_calls.append({
|
|
357
|
+
"id": tc["id"] or f"call_{i}",
|
|
358
|
+
"type": "function",
|
|
359
|
+
"function": {
|
|
360
|
+
"name": tc["function"]["name"] or "",
|
|
361
|
+
"arguments": tc["function"]["arguments"]
|
|
362
|
+
}
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
partial_msg = Message(
|
|
366
|
+
role=ContentRole.ASSISTANT,
|
|
367
|
+
content=aggregated_text or "",
|
|
368
|
+
tool_calls=None if not message_tool_calls else [
|
|
369
|
+
ToolCall(
|
|
370
|
+
id=mc["id"],
|
|
371
|
+
type="function",
|
|
372
|
+
function=ToolCallFunction(
|
|
373
|
+
name=mc["function"]["name"],
|
|
374
|
+
arguments=mc["function"]["arguments"],
|
|
375
|
+
),
|
|
376
|
+
) for mc in message_tool_calls
|
|
377
|
+
],
|
|
378
|
+
)
|
|
379
|
+
try:
|
|
380
|
+
if config.on_event:
|
|
381
|
+
config.on_event(AssistantMessageEvent(data=to_event_data(
|
|
382
|
+
AssistantMessageEventData(message=partial_msg)
|
|
383
|
+
)))
|
|
384
|
+
except Exception as _e:
|
|
385
|
+
# Do not fail the run on callback errors
|
|
386
|
+
pass
|
|
387
|
+
|
|
388
|
+
# Build final response object compatible with downstream logic
|
|
389
|
+
final_tool_calls = None
|
|
390
|
+
if len(partial_tool_calls) > 0:
|
|
391
|
+
final_tool_calls = []
|
|
392
|
+
for i, tc in enumerate(partial_tool_calls):
|
|
393
|
+
final_tool_calls.append({
|
|
394
|
+
"id": tc["id"] or f"call_{i}",
|
|
395
|
+
"type": "function",
|
|
396
|
+
"function": {
|
|
397
|
+
"name": tc["function"]["name"] or "",
|
|
398
|
+
"arguments": tc["function"]["arguments"]
|
|
399
|
+
}
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
llm_response = {
|
|
403
|
+
"message": {
|
|
404
|
+
"content": aggregated_text or None,
|
|
405
|
+
"tool_calls": final_tool_calls
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
except Exception:
|
|
409
|
+
# Fallback to non-streaming on error
|
|
410
|
+
assistant_event_streamed = False
|
|
411
|
+
llm_response = await config.model_provider.get_completion(state, current_agent, config)
|
|
412
|
+
else:
|
|
413
|
+
llm_response = await config.model_provider.get_completion(state, current_agent, config)
|
|
305
414
|
|
|
306
415
|
# Emit LLM call end event
|
|
307
416
|
if config.on_event:
|
jaf/core/tools.py
CHANGED
|
@@ -89,14 +89,12 @@ def create_function_tool(config: FunctionToolConfig) -> Tool:
|
|
|
89
89
|
# Validate schema generation (cached for performance)
|
|
90
90
|
if not hasattr(parameters, '_schema_validated'):
|
|
91
91
|
try:
|
|
92
|
+
# Generate schema once to validate the model is well-formed.
|
|
93
|
+
# Allow empty object schemas (no parameters) for tools that take no args.
|
|
92
94
|
if hasattr(parameters, 'model_json_schema'):
|
|
93
|
-
|
|
94
|
-
if not test_schema.get('properties'):
|
|
95
|
-
raise ValueError(f"Tool '{tool_name}' has no properties in schema. Check your Pydantic model fields.")
|
|
95
|
+
_ = parameters.model_json_schema()
|
|
96
96
|
elif hasattr(parameters, 'schema'):
|
|
97
|
-
|
|
98
|
-
if not test_schema.get('properties'):
|
|
99
|
-
raise ValueError(f"Tool '{tool_name}' has no properties in schema. Check your Pydantic model fields.")
|
|
97
|
+
_ = parameters.schema()
|
|
100
98
|
parameters._schema_validated = True
|
|
101
99
|
except Exception as e:
|
|
102
100
|
logger.error(f"Tool {tool_name} schema generation failed: {e}")
|
jaf/core/tracing.py
CHANGED
|
@@ -364,7 +364,6 @@ class LangfuseTraceCollector:
|
|
|
364
364
|
user_id = None
|
|
365
365
|
|
|
366
366
|
# Debug: Print the event data structure to understand what we're working with
|
|
367
|
-
print(f"[LANGFUSE DEBUG] Event data keys: {list(event.data.keys()) if event.data else 'No data'}")
|
|
368
367
|
if event.data.get("context"):
|
|
369
368
|
context = event.data["context"]
|
|
370
369
|
print(f"[LANGFUSE DEBUG] Context type: {type(context)}")
|
|
@@ -649,7 +648,6 @@ class LangfuseTraceCollector:
|
|
|
649
648
|
return TraceId(event.data['runId'])
|
|
650
649
|
|
|
651
650
|
# Debug: print what's actually in the event data
|
|
652
|
-
print(f"[LANGFUSE] Event data keys: {list(event.data.keys()) if hasattr(event, 'data') and event.data else 'No data'}")
|
|
653
651
|
return None
|
|
654
652
|
|
|
655
653
|
def _get_span_id(self, event: TraceEvent) -> str:
|
jaf/core/types.py
CHANGED
|
@@ -5,7 +5,7 @@ This module defines all the fundamental data structures and types used throughou
|
|
|
5
5
|
the framework, maintaining immutability and type safety.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from collections.abc import Awaitable
|
|
8
|
+
from collections.abc import Awaitable, AsyncIterator
|
|
9
9
|
|
|
10
10
|
# ReadOnly is only available in Python 3.13+, so we'll use a simpler approach
|
|
11
11
|
from dataclasses import dataclass, field
|
|
@@ -416,6 +416,15 @@ class LLMCallEndEvent:
|
|
|
416
416
|
data: LLMCallEndEventData = field(default_factory=lambda: LLMCallEndEventData(None, TraceId(""), RunId("")))
|
|
417
417
|
|
|
418
418
|
@dataclass(frozen=True)
|
|
419
|
+
class AssistantMessageEventData:
|
|
420
|
+
"""Data for assistant message events (partial or complete)."""
|
|
421
|
+
message: Message
|
|
422
|
+
|
|
423
|
+
@dataclass(frozen=True)
|
|
424
|
+
class AssistantMessageEvent:
|
|
425
|
+
type: Literal['assistant_message'] = 'assistant_message'
|
|
426
|
+
data: AssistantMessageEventData = field(default_factory=lambda: AssistantMessageEventData(Message(role=ContentRole.ASSISTANT, content="")))
|
|
427
|
+
@dataclass(frozen=True)
|
|
419
428
|
class ToolCallStartEventData:
|
|
420
429
|
"""Data for tool call start events."""
|
|
421
430
|
tool_name: str
|
|
@@ -515,6 +524,7 @@ TraceEvent = Union[
|
|
|
515
524
|
OutputParseEvent,
|
|
516
525
|
LLMCallStartEvent,
|
|
517
526
|
LLMCallEndEvent,
|
|
527
|
+
AssistantMessageEvent,
|
|
518
528
|
ToolCallStartEvent,
|
|
519
529
|
ToolCallEndEvent,
|
|
520
530
|
HandoffEvent,
|
|
@@ -532,6 +542,30 @@ class ModelCompletionResponse:
|
|
|
532
542
|
"""Response structure from model completion."""
|
|
533
543
|
message: Optional[ModelCompletionMessage] = None
|
|
534
544
|
|
|
545
|
+
# Streaming chunk structures for provider-level streaming support
|
|
546
|
+
@dataclass(frozen=True)
|
|
547
|
+
class ToolCallFunctionDelta:
|
|
548
|
+
"""Function fields that may stream as deltas."""
|
|
549
|
+
name: Optional[str] = None
|
|
550
|
+
arguments_delta: Optional[str] = None
|
|
551
|
+
|
|
552
|
+
@dataclass(frozen=True)
|
|
553
|
+
class ToolCallDelta:
|
|
554
|
+
"""Represents a partial tool call delta in a streamed response."""
|
|
555
|
+
index: int
|
|
556
|
+
id: Optional[str] = None
|
|
557
|
+
type: Literal['function'] = 'function'
|
|
558
|
+
function: Optional[ToolCallFunctionDelta] = None
|
|
559
|
+
|
|
560
|
+
@dataclass(frozen=True)
|
|
561
|
+
class CompletionStreamChunk:
|
|
562
|
+
"""A streamed chunk from the model provider."""
|
|
563
|
+
delta: Optional[str] = None
|
|
564
|
+
tool_call_delta: Optional[ToolCallDelta] = None
|
|
565
|
+
is_done: Optional[bool] = False
|
|
566
|
+
finish_reason: Optional[str] = None
|
|
567
|
+
raw: Optional[Any] = None
|
|
568
|
+
|
|
535
569
|
@runtime_checkable
|
|
536
570
|
class ModelProvider(Protocol[Ctx]):
|
|
537
571
|
"""Protocol for model providers."""
|
|
@@ -545,6 +579,15 @@ class ModelProvider(Protocol[Ctx]):
|
|
|
545
579
|
"""Get completion from the model."""
|
|
546
580
|
...
|
|
547
581
|
|
|
582
|
+
async def get_completion_stream(
|
|
583
|
+
self,
|
|
584
|
+
state: RunState[Ctx],
|
|
585
|
+
agent: Agent[Ctx, Any],
|
|
586
|
+
config: 'RunConfig[Ctx]'
|
|
587
|
+
) -> AsyncIterator[CompletionStreamChunk]:
|
|
588
|
+
"""Optional streaming API: yields incremental deltas while generating."""
|
|
589
|
+
...
|
|
590
|
+
|
|
548
591
|
@dataclass(frozen=True)
|
|
549
592
|
class RunConfig(Generic[Ctx]):
|
|
550
593
|
"""Configuration for running agents."""
|
jaf/providers/model.py
CHANGED
|
@@ -5,13 +5,14 @@ This module provides model providers that integrate with various LLM services,
|
|
|
5
5
|
starting with LiteLLM for multi-provider support.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Any, Dict, Optional, TypeVar
|
|
8
|
+
from typing import Any, Dict, Optional, TypeVar, AsyncIterator, List
|
|
9
|
+
import asyncio
|
|
9
10
|
import httpx
|
|
10
11
|
|
|
11
12
|
from openai import OpenAI
|
|
12
13
|
from pydantic import BaseModel
|
|
13
14
|
|
|
14
|
-
from ..core.types import Agent, ContentRole, Message, ModelProvider, RunConfig, RunState
|
|
15
|
+
from ..core.types import Agent, ContentRole, Message, ModelProvider, RunConfig, RunState, CompletionStreamChunk, ToolCallDelta, ToolCallFunctionDelta
|
|
15
16
|
from ..core.proxy import ProxyConfig
|
|
16
17
|
|
|
17
18
|
Ctx = TypeVar('Ctx')
|
|
@@ -169,6 +170,168 @@ def make_litellm_provider(
|
|
|
169
170
|
'prompt': messages
|
|
170
171
|
}
|
|
171
172
|
|
|
173
|
+
async def get_completion_stream(
|
|
174
|
+
self,
|
|
175
|
+
state: RunState[Ctx],
|
|
176
|
+
agent: Agent[Ctx, Any],
|
|
177
|
+
config: RunConfig[Ctx]
|
|
178
|
+
) -> AsyncIterator[CompletionStreamChunk]:
|
|
179
|
+
"""
|
|
180
|
+
Stream completion chunks from the model provider, yielding text deltas and tool-call deltas.
|
|
181
|
+
Uses OpenAI-compatible streaming via LiteLLM endpoint.
|
|
182
|
+
"""
|
|
183
|
+
# Determine model to use
|
|
184
|
+
model = (config.model_override or
|
|
185
|
+
(agent.model_config.name if agent.model_config else "gpt-4o"))
|
|
186
|
+
|
|
187
|
+
# Create system message
|
|
188
|
+
system_message = {
|
|
189
|
+
"role": "system",
|
|
190
|
+
"content": agent.instructions(state)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Convert messages to OpenAI format
|
|
194
|
+
messages = [system_message] + [
|
|
195
|
+
_convert_message(msg) for msg in state.messages
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
# Convert tools to OpenAI format
|
|
199
|
+
tools = None
|
|
200
|
+
if agent.tools:
|
|
201
|
+
tools = [
|
|
202
|
+
{
|
|
203
|
+
"type": "function",
|
|
204
|
+
"function": {
|
|
205
|
+
"name": tool.schema.name,
|
|
206
|
+
"description": tool.schema.description,
|
|
207
|
+
"parameters": _pydantic_to_json_schema(tool.schema.parameters),
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for tool in agent.tools
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
# Determine tool choice behavior
|
|
214
|
+
last_message = state.messages[-1] if state.messages else None
|
|
215
|
+
is_after_tool_call = last_message and (last_message.role == ContentRole.TOOL or last_message.role == 'tool')
|
|
216
|
+
|
|
217
|
+
# Prepare request parameters
|
|
218
|
+
request_params: Dict[str, Any] = {
|
|
219
|
+
"model": model,
|
|
220
|
+
"messages": messages,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Add optional parameters
|
|
224
|
+
if agent.model_config:
|
|
225
|
+
if agent.model_config.temperature is not None:
|
|
226
|
+
request_params["temperature"] = agent.model_config.temperature
|
|
227
|
+
if agent.model_config.max_tokens is not None:
|
|
228
|
+
request_params["max_tokens"] = agent.model_config.max_tokens
|
|
229
|
+
|
|
230
|
+
if tools:
|
|
231
|
+
request_params["tools"] = tools
|
|
232
|
+
# Set tool_choice to auto when tools are available
|
|
233
|
+
request_params["tool_choice"] = "auto"
|
|
234
|
+
|
|
235
|
+
if agent.output_codec:
|
|
236
|
+
request_params["response_format"] = {"type": "json_object"}
|
|
237
|
+
|
|
238
|
+
# Enable streaming
|
|
239
|
+
request_params["stream"] = True
|
|
240
|
+
|
|
241
|
+
loop = asyncio.get_running_loop()
|
|
242
|
+
queue: asyncio.Queue = asyncio.Queue(maxsize=256)
|
|
243
|
+
SENTINEL = object()
|
|
244
|
+
|
|
245
|
+
def _put(item: CompletionStreamChunk):
|
|
246
|
+
try:
|
|
247
|
+
asyncio.run_coroutine_threadsafe(queue.put(item), loop)
|
|
248
|
+
except RuntimeError:
|
|
249
|
+
# Event loop closed; drop silently
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
def _producer():
|
|
253
|
+
try:
|
|
254
|
+
stream = self.client.chat.completions.create(**request_params)
|
|
255
|
+
for chunk in stream:
|
|
256
|
+
try:
|
|
257
|
+
# Best-effort extraction of raw for debugging
|
|
258
|
+
try:
|
|
259
|
+
raw_obj = chunk.model_dump() # pydantic BaseModel
|
|
260
|
+
except Exception:
|
|
261
|
+
raw_obj = None
|
|
262
|
+
|
|
263
|
+
choice = None
|
|
264
|
+
if getattr(chunk, "choices", None):
|
|
265
|
+
choice = chunk.choices[0]
|
|
266
|
+
|
|
267
|
+
if choice is None:
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
delta = getattr(choice, "delta", None)
|
|
271
|
+
finish_reason = getattr(choice, "finish_reason", None)
|
|
272
|
+
|
|
273
|
+
# Text content delta
|
|
274
|
+
if delta is not None:
|
|
275
|
+
content_delta = getattr(delta, "content", None)
|
|
276
|
+
if content_delta:
|
|
277
|
+
_put(CompletionStreamChunk(delta=content_delta, raw=raw_obj))
|
|
278
|
+
|
|
279
|
+
# Tool call deltas
|
|
280
|
+
tool_calls = getattr(delta, "tool_calls", None)
|
|
281
|
+
if isinstance(tool_calls, list):
|
|
282
|
+
for tc in tool_calls:
|
|
283
|
+
# Each tc is likely a pydantic model with .index/.id/.function
|
|
284
|
+
try:
|
|
285
|
+
idx = getattr(tc, "index", 0) or 0
|
|
286
|
+
tc_id = getattr(tc, "id", None)
|
|
287
|
+
fn = getattr(tc, "function", None)
|
|
288
|
+
fn_name = getattr(fn, "name", None) if fn is not None else None
|
|
289
|
+
# OpenAI streams "arguments" as incremental deltas
|
|
290
|
+
args_delta = getattr(fn, "arguments", None) if fn is not None else None
|
|
291
|
+
|
|
292
|
+
_put(CompletionStreamChunk(
|
|
293
|
+
tool_call_delta=ToolCallDelta(
|
|
294
|
+
index=idx,
|
|
295
|
+
id=tc_id,
|
|
296
|
+
type='function',
|
|
297
|
+
function=ToolCallFunctionDelta(
|
|
298
|
+
name=fn_name,
|
|
299
|
+
arguments_delta=args_delta
|
|
300
|
+
)
|
|
301
|
+
),
|
|
302
|
+
raw=raw_obj
|
|
303
|
+
))
|
|
304
|
+
except Exception:
|
|
305
|
+
# Skip malformed tool-call deltas
|
|
306
|
+
continue
|
|
307
|
+
|
|
308
|
+
# Completion ended
|
|
309
|
+
if finish_reason:
|
|
310
|
+
_put(CompletionStreamChunk(is_done=True, finish_reason=finish_reason, raw=raw_obj))
|
|
311
|
+
except Exception:
|
|
312
|
+
# Skip individual chunk errors, keep streaming
|
|
313
|
+
continue
|
|
314
|
+
except Exception:
|
|
315
|
+
# On top-level stream error, signal done
|
|
316
|
+
pass
|
|
317
|
+
finally:
|
|
318
|
+
try:
|
|
319
|
+
asyncio.run_coroutine_threadsafe(queue.put(SENTINEL), loop)
|
|
320
|
+
except RuntimeError:
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
# Start producer in background
|
|
324
|
+
loop.run_in_executor(None, _producer)
|
|
325
|
+
|
|
326
|
+
# Consume queue and yield
|
|
327
|
+
while True:
|
|
328
|
+
item = await queue.get()
|
|
329
|
+
if item is SENTINEL:
|
|
330
|
+
break
|
|
331
|
+
# Guarantee type for consumers
|
|
332
|
+
if isinstance(item, CompletionStreamChunk):
|
|
333
|
+
yield item
|
|
334
|
+
|
|
172
335
|
return LiteLLMProvider()
|
|
173
336
|
|
|
174
337
|
def _convert_message(msg: Message) -> Dict[str, Any]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jaf-py
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: A purely functional agent framework with immutable state and composable tools - Python implementation
|
|
5
5
|
Author: JAF Contributors
|
|
6
6
|
Maintainer: JAF Contributors
|
|
@@ -73,67 +73,67 @@ Dynamic: license-file
|
|
|
73
73
|
|
|
74
74
|
<!--  -->
|
|
75
75
|
|
|
76
|
-
[](https://github.com/xynehq/jaf-py)
|
|
77
77
|
[](https://www.python.org/)
|
|
78
78
|
[](https://xynehq.github.io/jaf-py/)
|
|
79
79
|
|
|
80
80
|
A purely functional agent framework with immutable state and composable tools, professionally converted from TypeScript to Python. JAF enables building production-ready AI agent systems with built-in security, observability, and error handling.
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
##
|
|
85
|
-
|
|
86
|
-
**[
|
|
87
|
-
|
|
88
|
-
##
|
|
89
|
-
|
|
90
|
-
###
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
###
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
|
|
102
|
-
###
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
|
|
108
|
-
###
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
|
|
115
|
-
###
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
|
|
123
|
-
###
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
|
|
129
|
-
###
|
|
130
|
-
-
|
|
131
|
-
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
|
|
136
|
-
##
|
|
82
|
+
**Production Ready**: Complete feature parity with TypeScript version, comprehensive test suite, and production deployment support.
|
|
83
|
+
|
|
84
|
+
## **[Read the Full Documentation](https://xynehq.github.io/jaf-py/)**
|
|
85
|
+
|
|
86
|
+
**[ Get Started →](https://xynehq.github.io/jaf-py/getting-started/)** | **[ API Reference →](https://xynehq.github.io/jaf-py/api-reference/)** | **[ Examples →](https://xynehq.github.io/jaf-py/examples/)**
|
|
87
|
+
|
|
88
|
+
## Key Features
|
|
89
|
+
|
|
90
|
+
### **Complete TypeScript Conversion**
|
|
91
|
+
- **Full Feature Parity**: All TypeScript functionality converted to Python
|
|
92
|
+
- **Type Safety**: Pydantic models with runtime validation
|
|
93
|
+
- **Immutable State**: Functional programming principles preserved
|
|
94
|
+
- **Tool Integration**: Complete tool calling and execution system
|
|
95
|
+
|
|
96
|
+
### **Production Ready Server**
|
|
97
|
+
- **FastAPI Server**: High-performance async HTTP API
|
|
98
|
+
- **Auto Documentation**: Interactive API docs at `/docs`
|
|
99
|
+
- **Health Monitoring**: Built-in health checks and metrics
|
|
100
|
+
- **CORS Support**: Ready for browser integration
|
|
101
|
+
|
|
102
|
+
### **Model Context Protocol (MCP)**
|
|
103
|
+
- **MCP Client**: Full MCP specification support
|
|
104
|
+
- **Stdio & SSE**: Multiple transport protocols
|
|
105
|
+
- **Tool Integration**: Seamless MCP tool integration
|
|
106
|
+
- **Auto Discovery**: Dynamic tool loading from MCP servers
|
|
107
|
+
|
|
108
|
+
### **Enterprise Security**
|
|
109
|
+
- **Input Guardrails**: Content filtering and validation
|
|
110
|
+
- **Output Guardrails**: Response sanitization
|
|
111
|
+
- **Permission System**: Role-based access control
|
|
112
|
+
- **Audit Logging**: Complete interaction tracing
|
|
113
|
+
- **Proxy Support**: Corporate proxy integration with authentication
|
|
114
|
+
|
|
115
|
+
### **Observability & Monitoring**
|
|
116
|
+
- **Real-time Tracing**: Event-driven observability
|
|
117
|
+
- **OpenTelemetry Integration**: Distributed tracing with OTLP
|
|
118
|
+
- **Langfuse Tracing**: LLM observability and analytics
|
|
119
|
+
- **Structured Logging**: JSON-formatted logs
|
|
120
|
+
- **Error Handling**: Comprehensive error types and recovery
|
|
121
|
+
- **Performance Metrics**: Built-in timing and counters
|
|
122
|
+
|
|
123
|
+
### **Agent-as-Tool Architecture**
|
|
124
|
+
- **Hierarchical Orchestration**: Use agents as tools in other agents
|
|
125
|
+
- **Conditional Tool Enabling**: Enable/disable agent tools based on context
|
|
126
|
+
- **Session Management**: Configurable session inheritance for sub-agents
|
|
127
|
+
- **Flexible Output Extraction**: Custom extractors for agent tool outputs
|
|
128
|
+
|
|
129
|
+
### **Developer Experience**
|
|
130
|
+
- **CLI Tools**: Project initialization and management
|
|
131
|
+
- **Hot Reload**: Development server with auto-reload
|
|
132
|
+
- **Type Hints**: Full mypy compatibility
|
|
133
|
+
- **Rich Examples**: RAG, multi-agent, agent-as-tool, and server demos
|
|
134
|
+
- **Visual Architecture**: Graphviz-powered agent and tool diagrams
|
|
135
|
+
|
|
136
|
+
## Core Philosophy
|
|
137
137
|
|
|
138
138
|
- **Immutability**: All core data structures are deeply immutable
|
|
139
139
|
- **Pure Functions**: Core logic expressed as pure, predictable functions
|
|
@@ -141,7 +141,7 @@ A purely functional agent framework with immutable state and composable tools, p
|
|
|
141
141
|
- **Composition over Configuration**: Build complex behavior by composing simple functions
|
|
142
142
|
- **Type-Safe by Design**: Leverages Python's type system with Pydantic for runtime safety
|
|
143
143
|
|
|
144
|
-
##
|
|
144
|
+
## Quick Start
|
|
145
145
|
|
|
146
146
|
### Installation
|
|
147
147
|
|
|
@@ -197,37 +197,37 @@ pip install -r requirements-docs.txt
|
|
|
197
197
|
./docs.sh deploy # Deploy to GitHub Pages
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
##
|
|
200
|
+
## Documentation
|
|
201
201
|
|
|
202
|
-
###
|
|
202
|
+
### **[Official Documentation Website](https://xynehq.github.io/jaf-py/)**
|
|
203
203
|
|
|
204
204
|
The complete, searchable documentation is available at **[xynehq.github.io/jaf-py](https://xynehq.github.io/jaf-py/)** with:
|
|
205
205
|
|
|
206
|
-
-
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
-
|
|
210
|
-
-
|
|
211
|
-
-
|
|
206
|
+
- **Interactive navigation** with search and filtering
|
|
207
|
+
- **Dark/light mode** with automatic system preference detection
|
|
208
|
+
- **Mobile-responsive design** for documentation on any device
|
|
209
|
+
- **Live code examples** with syntax highlighting
|
|
210
|
+
- **API reference** with auto-generated documentation
|
|
211
|
+
- **Always up-to-date** with automatic deployments
|
|
212
212
|
|
|
213
|
-
###
|
|
213
|
+
### **Local Documentation**
|
|
214
214
|
|
|
215
215
|
For offline access, documentation is also available in the [`docs/`](docs/) directory:
|
|
216
216
|
|
|
217
|
-
- **[
|
|
218
|
-
- **[
|
|
219
|
-
- **[
|
|
220
|
-
- **[
|
|
221
|
-
- **[
|
|
222
|
-
- **[
|
|
223
|
-
- **[
|
|
224
|
-
- **[
|
|
225
|
-
- **[
|
|
226
|
-
- **[
|
|
227
|
-
- **[
|
|
228
|
-
- **[
|
|
229
|
-
|
|
230
|
-
##
|
|
217
|
+
- **[ Documentation Hub](docs/README.md)** - Your starting point for all documentation
|
|
218
|
+
- **[ Getting Started](docs/getting-started.md)** - Installation and first agent tutorial
|
|
219
|
+
- **[ Core Concepts](docs/core-concepts.md)** - JAF's functional architecture principles
|
|
220
|
+
- **[ API Reference](docs/api-reference.md)** - Complete Python API documentation
|
|
221
|
+
- **[ Tools Guide](docs/tools.md)** - Creating and using tools
|
|
222
|
+
- **[ Memory System](docs/memory-system.md)** - Persistence and memory providers
|
|
223
|
+
- **[ Model Providers](docs/model-providers.md)** - LiteLLM integration
|
|
224
|
+
- **[ Monitoring](docs/monitoring.md)** - Observability, metrics, and alerting
|
|
225
|
+
- **[ Server API](docs/server-api.md)** - FastAPI endpoints reference
|
|
226
|
+
- **[ Deployment](docs/deployment.md)** - Production deployment guide
|
|
227
|
+
- **[ Examples](docs/examples.md)** - Detailed example walkthroughs
|
|
228
|
+
- **[ Troubleshooting](docs/troubleshooting.md)** - Common issues and solutions
|
|
229
|
+
|
|
230
|
+
## Project Structure
|
|
231
231
|
|
|
232
232
|
```
|
|
233
233
|
jaf-py/
|
|
@@ -243,7 +243,7 @@ jaf-py/
|
|
|
243
243
|
└── tests/ # Test suite
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
-
##
|
|
246
|
+
## Architectural Visualization
|
|
247
247
|
|
|
248
248
|
JAF includes powerful visualization capabilities to help you understand and document your agent systems.
|
|
249
249
|
|
|
@@ -295,7 +295,7 @@ async def main():
|
|
|
295
295
|
)
|
|
296
296
|
|
|
297
297
|
if result.success:
|
|
298
|
-
print(f"
|
|
298
|
+
print(f" Visualization saved to: {result.output_path}")
|
|
299
299
|
else:
|
|
300
300
|
print(f"❌ Error: {result.error}")
|
|
301
301
|
|
|
@@ -304,12 +304,12 @@ asyncio.run(main())
|
|
|
304
304
|
|
|
305
305
|
### Features
|
|
306
306
|
|
|
307
|
-
-
|
|
308
|
-
-
|
|
309
|
-
-
|
|
310
|
-
-
|
|
311
|
-
-
|
|
312
|
-
-
|
|
307
|
+
- **Multiple Color Schemes**: Choose from `default`, `modern`, or `minimal` themes
|
|
308
|
+
- **Agent Architecture**: Visualize agents, tools, and handoff relationships
|
|
309
|
+
- **Tool Ecosystems**: Generate dedicated tool interaction diagrams
|
|
310
|
+
- **Runner Architecture**: Show complete system architecture with session layers
|
|
311
|
+
- **Multiple Formats**: Export as PNG, SVG, or PDF
|
|
312
|
+
- **Customizable Layouts**: Support for various Graphviz layouts (`dot`, `circo`, `neato`, etc.)
|
|
313
313
|
|
|
314
314
|
### Example Output
|
|
315
315
|
|
|
@@ -336,7 +336,7 @@ await run_visualization_examples()
|
|
|
336
336
|
# - ./examples/agent-modern.png (modern color scheme)
|
|
337
337
|
```
|
|
338
338
|
|
|
339
|
-
##
|
|
339
|
+
## Key Components
|
|
340
340
|
|
|
341
341
|
### Core Types
|
|
342
342
|
|
|
@@ -414,7 +414,7 @@ async def main():
|
|
|
414
414
|
asyncio.run(main())
|
|
415
415
|
```
|
|
416
416
|
|
|
417
|
-
##
|
|
417
|
+
## Security & Validation
|
|
418
418
|
|
|
419
419
|
### Composable Validation Policies
|
|
420
420
|
|
|
@@ -449,7 +449,7 @@ config = RunConfig(
|
|
|
449
449
|
)
|
|
450
450
|
```
|
|
451
451
|
|
|
452
|
-
##
|
|
452
|
+
## Agent-as-Tool Functionality
|
|
453
453
|
|
|
454
454
|
JAF 2.2+ introduces powerful agent-as-tool capabilities, allowing you to use agents as tools within other agents for hierarchical orchestration:
|
|
455
455
|
|
|
@@ -528,7 +528,7 @@ def create_triage_agent():
|
|
|
528
528
|
)
|
|
529
529
|
```
|
|
530
530
|
|
|
531
|
-
##
|
|
531
|
+
## Observability
|
|
532
532
|
|
|
533
533
|
### Real-time Tracing
|
|
534
534
|
|
|
@@ -605,7 +605,7 @@ if result.outcome.status == 'error':
|
|
|
605
605
|
print(f"[{severity}] {formatted_error} (retryable: {is_retryable})")
|
|
606
606
|
```
|
|
607
607
|
|
|
608
|
-
##
|
|
608
|
+
## Provider Integrations
|
|
609
609
|
|
|
610
610
|
### A2A (Agent-to-Agent) Communication
|
|
611
611
|
|
|
@@ -672,7 +672,7 @@ from jaf.providers.mcp import create_mcp_sse_client
|
|
|
672
672
|
sse_client = create_mcp_sse_client('http://localhost:8080/sse')
|
|
673
673
|
```
|
|
674
674
|
|
|
675
|
-
##
|
|
675
|
+
## Development Server
|
|
676
676
|
|
|
677
677
|
JAF includes a built-in development server for testing agents locally via HTTP endpoints:
|
|
678
678
|
|
|
@@ -706,7 +706,7 @@ Server provides RESTful endpoints:
|
|
|
706
706
|
- `POST /chat` - General chat endpoint
|
|
707
707
|
- `POST /agents/{name}/chat` - Agent-specific endpoint
|
|
708
708
|
|
|
709
|
-
##
|
|
709
|
+
## Function Composition
|
|
710
710
|
|
|
711
711
|
JAF supports functional composition patterns for building complex behaviors from simple, reusable functions:
|
|
712
712
|
|
|
@@ -742,7 +742,7 @@ enhanced_tool = create_function_tool({
|
|
|
742
742
|
- **Type Safety**: Full type checking support
|
|
743
743
|
- **Performance**: Optimize individual pieces independently
|
|
744
744
|
|
|
745
|
-
##
|
|
745
|
+
## Example Applications
|
|
746
746
|
|
|
747
747
|
Explore the example applications to see the framework in action:
|
|
748
748
|
|
|
@@ -754,12 +754,12 @@ python server_example.py
|
|
|
754
754
|
```
|
|
755
755
|
|
|
756
756
|
**Features demonstrated:**
|
|
757
|
-
-
|
|
758
|
-
-
|
|
759
|
-
-
|
|
760
|
-
-
|
|
761
|
-
-
|
|
762
|
-
-
|
|
757
|
+
- Multiple specialized agents (math, weather, general)
|
|
758
|
+
- Tool integration (calculator, weather API)
|
|
759
|
+
- Agent handoffs and routing
|
|
760
|
+
- RESTful API with auto-documentation
|
|
761
|
+
- Real-time tracing and error handling
|
|
762
|
+
- Production-ready server configuration
|
|
763
763
|
|
|
764
764
|
**Available endpoints:**
|
|
765
765
|
- `GET /health` - Server health check
|
|
@@ -778,11 +778,11 @@ python agent_as_tool_example.py --server
|
|
|
778
778
|
```
|
|
779
779
|
|
|
780
780
|
**Features demonstrated:**
|
|
781
|
-
-
|
|
782
|
-
-
|
|
783
|
-
-
|
|
784
|
-
-
|
|
785
|
-
-
|
|
781
|
+
- Hierarchical agent orchestration
|
|
782
|
+
- Conditional tool enabling based on context
|
|
783
|
+
- Custom output extraction from agent tools
|
|
784
|
+
- Session management for sub-agents
|
|
785
|
+
- Translation agents working together
|
|
786
786
|
|
|
787
787
|
### 3. Tracing Integration Demos
|
|
788
788
|
|
|
@@ -796,10 +796,10 @@ python langfuse_tracing_demo.py
|
|
|
796
796
|
```
|
|
797
797
|
|
|
798
798
|
**Features demonstrated:**
|
|
799
|
-
-
|
|
800
|
-
-
|
|
801
|
-
-
|
|
802
|
-
-
|
|
799
|
+
- OpenTelemetry distributed tracing setup
|
|
800
|
+
- Langfuse LLM observability integration
|
|
801
|
+
- Composite trace collectors
|
|
802
|
+
- Real-time monitoring and analytics
|
|
803
803
|
|
|
804
804
|
### 4. MCP Integration Demo
|
|
805
805
|
|
|
@@ -809,12 +809,12 @@ python main.py
|
|
|
809
809
|
```
|
|
810
810
|
|
|
811
811
|
**Features demonstrated:**
|
|
812
|
-
-
|
|
813
|
-
-
|
|
814
|
-
-
|
|
815
|
-
-
|
|
812
|
+
- Model Context Protocol integration
|
|
813
|
+
- Dynamic tool loading from MCP servers
|
|
814
|
+
- Secure filesystem operations
|
|
815
|
+
- MCP client configuration and management
|
|
816
816
|
|
|
817
|
-
##
|
|
817
|
+
## Testing
|
|
818
818
|
|
|
819
819
|
```bash
|
|
820
820
|
pytest # Run tests
|
|
@@ -823,7 +823,7 @@ mypy . # Type checking
|
|
|
823
823
|
black . # Format code
|
|
824
824
|
```
|
|
825
825
|
|
|
826
|
-
##
|
|
826
|
+
## Architecture Principles
|
|
827
827
|
|
|
828
828
|
### Immutable State Machine
|
|
829
829
|
- All state transformations create new state objects
|
|
@@ -859,4 +859,4 @@ MIT
|
|
|
859
859
|
|
|
860
860
|
---
|
|
861
861
|
|
|
862
|
-
**JAF (Juspay Agentic Framework) v2.2** - Building the future of functional AI agent systems
|
|
862
|
+
**JAF (Juspay Agentic Framework) v2.2** - Building the future of functional AI agent systems
|
|
@@ -42,16 +42,16 @@ jaf/core/__init__.py,sha256=rBvP_7TGbJICDJnA7a3qyX8yQErCDWaGAn5WzpyH4gU,1339
|
|
|
42
42
|
jaf/core/agent_tool.py,sha256=8TcBuSxGmDTW5F_GhBU_m5S43nYqkjO4qTrNERraAig,11656
|
|
43
43
|
jaf/core/analytics.py,sha256=NrUfOLLTDIhOzdfc65ZqS9AJ4ZAP9BtNtga69q0YdYw,23265
|
|
44
44
|
jaf/core/composition.py,sha256=IVxRO1Q9nK7JRH32qQ4p8WMIUu66BhqPNrlTNMGFVwE,26317
|
|
45
|
-
jaf/core/engine.py,sha256=
|
|
45
|
+
jaf/core/engine.py,sha256=lsmvVDpmuyo7akALplvJvZvOonSzClCG-0EGbc7o5yQ,32061
|
|
46
46
|
jaf/core/errors.py,sha256=5fwTNhkojKRQ4wZj3lZlgDnAsrYyjYOwXJkIr5EGNUc,5539
|
|
47
47
|
jaf/core/performance.py,sha256=jedQmTEkrKMD6_Aw1h8PdG-5TsdYSFFT7Or6k5dmN2g,9974
|
|
48
48
|
jaf/core/proxy.py,sha256=_WM3cpRlSQLYpgSBrnY30UPMe2iZtlqDQ65kppE-WY0,4609
|
|
49
49
|
jaf/core/proxy_helpers.py,sha256=i7a5fAX9rLmO4FMBX51-yRkTFwfWedzQNgnLmeLUd_A,4370
|
|
50
50
|
jaf/core/streaming.py,sha256=c5o9iqpjoYV2LrUpG6qLWCYrWcP-DCcZsvMbyqKunp8,16089
|
|
51
51
|
jaf/core/tool_results.py,sha256=-bTOqOX02lMyslp5Z4Dmuhx0cLd5o7kgR88qK2HO_sw,11323
|
|
52
|
-
jaf/core/tools.py,sha256=
|
|
53
|
-
jaf/core/tracing.py,sha256=
|
|
54
|
-
jaf/core/types.py,sha256=
|
|
52
|
+
jaf/core/tools.py,sha256=84N9A7QQ3xxcOs2eUUot3nmCnt5i7iZT9VwkuzuFBxQ,16274
|
|
53
|
+
jaf/core/tracing.py,sha256=3ByTbpYMWHJku-NEasK5ncyMOdv7vHwmG6ybJP19pxE,31659
|
|
54
|
+
jaf/core/types.py,sha256=SdtvjBNdjyvKGvGpGeUMUnYVGMpriy3QnCHUwPkz3Sg,18596
|
|
55
55
|
jaf/core/workflows.py,sha256=Ul-82gzjIXtkhnSMSPv-8igikjkMtW1EBo9yrfodtvI,26294
|
|
56
56
|
jaf/memory/__init__.py,sha256=-L98xlvihurGAzF0DnXtkueDVvO_wV2XxxEwAWdAj50,1400
|
|
57
57
|
jaf/memory/factory.py,sha256=Fh6JyvQtCKe38DZV5-NnC9vPRCvzBgSSPFIGaX7Nt5E,2958
|
|
@@ -68,7 +68,7 @@ jaf/policies/handoff.py,sha256=KJYYuL9T6v6DECRhnsS2Je6q4Aj9_zC5d_KBnvEnZNE,8318
|
|
|
68
68
|
jaf/policies/validation.py,sha256=wn-7ynH10E5nk-_r1_kHIYHrBGmLX0EFr-FUTHrsxvc,10903
|
|
69
69
|
jaf/providers/__init__.py,sha256=j_o-Rubr8d9tNYlFWb6fvzkxIBl3JKK_iabj9wTFia0,2114
|
|
70
70
|
jaf/providers/mcp.py,sha256=WxcC8gUFpDBBYyhorMcc1jHq3xMDMBtnwyRPthfL0S0,13074
|
|
71
|
-
jaf/providers/model.py,sha256=
|
|
71
|
+
jaf/providers/model.py,sha256=nulXAItAptJX6andlZWWGw19iVaqgloVkRzGNRvIKPk,15137
|
|
72
72
|
jaf/server/__init__.py,sha256=K0vSgTfzn3oM54UX9BeAROpaksYY43mFtfjSXoQrhXA,339
|
|
73
73
|
jaf/server/main.py,sha256=CTb0ywbPIq9ELfay5MKChVR7BpIQOoEbPjPfpzo2aBQ,2152
|
|
74
74
|
jaf/server/server.py,sha256=o-n6dPWmlu4_v5ozVW3BTOm1b5kHBQhqRYlHh5sXL-k,8048
|
|
@@ -79,9 +79,9 @@ jaf/visualization/functional_core.py,sha256=zedMDZbvjuOugWwnh6SJ2stvRNQX1Hlkb9Ab
|
|
|
79
79
|
jaf/visualization/graphviz.py,sha256=WTOM6UP72-lVKwI4_SAr5-GCC3ouckxHv88ypCDQWJ0,12056
|
|
80
80
|
jaf/visualization/imperative_shell.py,sha256=GpMrAlMnLo2IQgyB2nardCz09vMvAzaYI46MyrvJ0i4,2593
|
|
81
81
|
jaf/visualization/types.py,sha256=QQcbVeQJLuAOXk8ynd08DXIS-PVCnv3R-XVE9iAcglw,1389
|
|
82
|
-
jaf_py-2.
|
|
83
|
-
jaf_py-2.
|
|
84
|
-
jaf_py-2.
|
|
85
|
-
jaf_py-2.
|
|
86
|
-
jaf_py-2.
|
|
87
|
-
jaf_py-2.
|
|
82
|
+
jaf_py-2.4.1.dist-info/licenses/LICENSE,sha256=LXUQBJxdyr-7C4bk9cQBwvsF_xwA-UVstDTKabpcjlI,1063
|
|
83
|
+
jaf_py-2.4.1.dist-info/METADATA,sha256=F38_7dDyrX_DFA71FLRmSkqzfoubdyGqUT_iKAjEF5w,27216
|
|
84
|
+
jaf_py-2.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
85
|
+
jaf_py-2.4.1.dist-info/entry_points.txt,sha256=OtIJeNJpb24kgGrqRx9szGgDx1vL9ayq8uHErmu7U5w,41
|
|
86
|
+
jaf_py-2.4.1.dist-info/top_level.txt,sha256=Xu1RZbGaM4_yQX7bpalo881hg7N_dybaOW282F15ruE,4
|
|
87
|
+
jaf_py-2.4.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|