jaf-py 2.5.1__py3-none-any.whl → 2.5.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 CHANGED
@@ -191,7 +191,7 @@ def generate_run_id() -> RunId:
191
191
  """Generate a new run ID."""
192
192
  return create_run_id(str(uuid.uuid4()))
193
193
 
194
- __version__ = "2.0.0"
194
+ __version__ = "2.5.3"
195
195
  __all__ = [
196
196
  # Core types and functions
197
197
  "TraceId", "RunId", "ValidationResult", "Message", "ModelConfig",
jaf/core/__init__.py CHANGED
@@ -22,6 +22,7 @@ from .parallel_agents import (
22
22
  create_domain_experts_tool,
23
23
  )
24
24
  from .proxy import ProxyConfig, ProxyAuth, create_proxy_config, get_default_proxy_config
25
+ from .handoff import handoff_tool, handoff, create_handoff_tool, is_handoff_request, extract_handoff_target
25
26
 
26
27
  __all__ = [
27
28
  "Agent",
@@ -52,6 +53,7 @@ __all__ = [
52
53
  "create_conditional_enabler",
53
54
  "create_default_output_extractor",
54
55
  "create_domain_experts_tool",
56
+ "create_handoff_tool",
55
57
  "create_json_output_extractor",
56
58
  "create_language_specialists_tool",
57
59
  "create_parallel_agents_tool",
@@ -59,8 +61,12 @@ __all__ = [
59
61
  "create_run_id",
60
62
  "create_simple_parallel_tool",
61
63
  "create_trace_id",
64
+ "extract_handoff_target",
62
65
  "get_current_run_config",
63
66
  "get_default_proxy_config",
67
+ "handoff",
68
+ "handoff_tool",
69
+ "is_handoff_request",
64
70
  "require_permissions",
65
71
  "run",
66
72
  "set_current_run_config",
jaf/core/handoff.py ADDED
@@ -0,0 +1,191 @@
1
+ """
2
+ Handoff system for JAF framework.
3
+
4
+ This module provides a simple, elegant handoff mechanism that allows agents
5
+ to seamlessly transfer control to other agents with clean state management.
6
+ """
7
+
8
+ import json
9
+ from typing import Any, Optional, TypeVar
10
+ from dataclasses import dataclass
11
+
12
+ from .types import Tool, ToolSchema, ToolSource
13
+
14
+ try:
15
+ from pydantic import BaseModel, Field
16
+ except ImportError:
17
+ BaseModel = None
18
+ Field = None
19
+
20
+ Ctx = TypeVar('Ctx')
21
+
22
+
23
+ def _create_handoff_json(agent_name: str, message: str = "") -> str:
24
+ """Create the JSON structure for handoff requests."""
25
+ return json.dumps({
26
+ "handoff_to": agent_name,
27
+ "message": message or f"Handing off to {agent_name}",
28
+ "type": "handoff"
29
+ })
30
+
31
+
32
+ if BaseModel is not None and Field is not None:
33
+ class _HandoffInput(BaseModel):
34
+ """Input parameters for handoff tool (Pydantic model)."""
35
+ agent_name: str = Field(description="Name of the agent to hand off to")
36
+ message: str = Field(description="Message or context to pass to the target agent")
37
+ else:
38
+ class _HandoffInput(object):
39
+ """Plain-Python fallback for handoff input when Pydantic is unavailable.
40
+
41
+ This class intentionally does not call Field() so it is safe to import
42
+ when Pydantic is not installed.
43
+ """
44
+ agent_name: str
45
+ message: str
46
+
47
+ def __init__(self, agent_name: str, message: str = ""):
48
+ self.agent_name = agent_name
49
+ self.message = message
50
+
51
+ HandoffInput = _HandoffInput
52
+
53
+
54
+ @dataclass
55
+ class HandoffResult:
56
+ """Result of a handoff operation."""
57
+ target_agent: str
58
+ message: str
59
+ success: bool = True
60
+ error: Optional[str] = None
61
+
62
+
63
+ class HandoffTool:
64
+ """A tool that enables agents to hand off to other agents."""
65
+
66
+ def __init__(self):
67
+ # Create schema
68
+ if BaseModel:
69
+ parameters_model = HandoffInput
70
+ else:
71
+ # Fallback schema when Pydantic is not available
72
+ parameters_model = {
73
+ "type": "object",
74
+ "properties": {
75
+ "agent_name": {
76
+ "type": "string",
77
+ "description": "Name of the agent to hand off to"
78
+ },
79
+ "message": {
80
+ "type": "string",
81
+ "description": "Message or context to pass to the target agent"
82
+ }
83
+ },
84
+ "required": ["agent_name", "message"]
85
+ }
86
+
87
+ self.schema = ToolSchema(
88
+ name="handoff",
89
+ description="Hand off the conversation to another agent",
90
+ parameters=parameters_model
91
+ )
92
+ self.source = ToolSource.NATIVE
93
+ self.metadata = {"type": "handoff", "system": True}
94
+
95
+ async def execute(self, args: HandoffInput, context: Any) -> str:
96
+ """
97
+ Execute the handoff.
98
+
99
+ Parameters:
100
+ args (HandoffInput): The handoff input arguments.
101
+ context (Any): Context containing current agent and run state information.
102
+ """
103
+ # Extract arguments
104
+ if hasattr(args, 'agent_name'):
105
+ agent_name = args.agent_name
106
+ message = args.message
107
+ elif isinstance(args, dict):
108
+ agent_name = args.get('agent_name', '')
109
+ message = args.get('message', '')
110
+ else:
111
+ return json.dumps({
112
+ "error": "invalid_handoff_args",
113
+ "message": "Invalid handoff arguments provided",
114
+ "usage": "handoff(agent_name='target_agent', message='optional context')"
115
+ })
116
+
117
+ if not agent_name:
118
+ return json.dumps({
119
+ "error": "missing_agent_name",
120
+ "message": "Agent name is required for handoff",
121
+ "usage": "handoff(agent_name='target_agent', message='optional context')"
122
+ })
123
+
124
+ # Add agent validation if we have access to current agent info
125
+ if context and hasattr(context, 'current_agent'):
126
+ current_agent = context.current_agent
127
+ if current_agent.handoffs and agent_name not in current_agent.handoffs:
128
+ return json.dumps({
129
+ "error": "handoff_not_allowed",
130
+ "message": f"Agent {current_agent.name} cannot handoff to {agent_name}",
131
+ "allowed_handoffs": current_agent.handoffs
132
+ })
133
+
134
+ # Return the special handoff JSON that the engine recognizes
135
+ return _create_handoff_json(agent_name, message)
136
+
137
+
138
+ def create_handoff_tool() -> Tool:
139
+ """Create a handoff tool that can be added to any agent."""
140
+ return HandoffTool()
141
+
142
+ handoff_tool = create_handoff_tool()
143
+
144
+ def handoff(agent_name: str, message: str = "") -> str:
145
+ """
146
+ Simple function to perform a handoff (for use in agent tools).
147
+
148
+ Args:
149
+ agent_name: Name of the agent to hand off to
150
+ message: Optional message to pass to the target agent
151
+
152
+ Returns:
153
+ JSON string that triggers a handoff
154
+ """
155
+ return _create_handoff_json(agent_name, message)
156
+
157
+
158
+ def is_handoff_request(result: str) -> bool:
159
+ """
160
+ Check if a tool result is a handoff request.
161
+
162
+ Args:
163
+ result: Tool execution result
164
+
165
+ Returns:
166
+ True if the result is a handoff request
167
+ """
168
+ try:
169
+ parsed = json.loads(result)
170
+ return isinstance(parsed, dict) and "handoff_to" in parsed
171
+ except (json.JSONDecodeError, TypeError):
172
+ return False
173
+
174
+
175
+ def extract_handoff_target(result: str) -> Optional[str]:
176
+ """
177
+ Extract the target agent name from a handoff result.
178
+
179
+ Args:
180
+ result: Tool execution result
181
+
182
+ Returns:
183
+ Target agent name if it's a handoff, None otherwise
184
+ """
185
+ try:
186
+ parsed = json.loads(result)
187
+ if isinstance(parsed, dict) and "handoff_to" in parsed:
188
+ return parsed["handoff_to"]
189
+ except (json.JSONDecodeError, TypeError):
190
+ pass
191
+ return None
jaf/core/state.py CHANGED
@@ -5,10 +5,113 @@ This module provides functions to manage approval state transitions
5
5
  and integrate with approval storage systems.
6
6
  """
7
7
 
8
- from typing import Dict, Any, Optional
8
+ from typing import Dict, Any, Optional, List
9
9
  from dataclasses import replace
10
10
 
11
- from .types import RunState, RunConfig, Interruption, ApprovalValue
11
+ from .types import RunState, RunConfig, Interruption, ApprovalValue, Message, ContentRole, Attachment
12
+
13
+
14
+ def _extract_attachments_from_messages(messages: List[Dict[str, Any]]) -> List[Attachment]:
15
+ """Extract attachment objects from message data."""
16
+ attachments = []
17
+
18
+ for msg in messages:
19
+ msg_attachments = msg.get('attachments', [])
20
+ for att in msg_attachments:
21
+ try:
22
+ # Convert dict to Attachment object
23
+ attachment = Attachment(
24
+ kind=att.get('kind', 'image'),
25
+ mime_type=att.get('mime_type'),
26
+ name=att.get('name'),
27
+ url=att.get('url'),
28
+ data=att.get('data'),
29
+ format=att.get('format'),
30
+ use_litellm_format=att.get('use_litellm_format')
31
+ )
32
+ attachments.append(attachment)
33
+ except Exception as e:
34
+ print(f"[JAF:APPROVAL] Failed to process attachment: {e}")
35
+
36
+ return attachments
37
+
38
+
39
+ def _process_additional_context_images(additional_context: Optional[Dict[str, Any]]) -> List[Attachment]:
40
+ """Process additional context and extract any image attachments."""
41
+ if not additional_context:
42
+ return []
43
+
44
+ attachments = []
45
+
46
+ # Handle messages with attachments
47
+ messages = additional_context.get('messages', [])
48
+ if messages:
49
+ attachments.extend(_extract_attachments_from_messages(messages))
50
+
51
+ # Handle legacy image_context format
52
+ image_context = additional_context.get('image_context')
53
+ if image_context and image_context.get('type') == 'image_url':
54
+ try:
55
+ image_url = image_context.get('image_url', {})
56
+ url = image_url.get('url', '')
57
+
58
+ if url.startswith('data:'):
59
+ # Parse data URL: data:image/png;base64,iVBORw0KGgo...
60
+ header, data = url.split(',', 1)
61
+ mime_type = header.split(':')[1].split(';')[0]
62
+
63
+ attachment = Attachment(
64
+ kind='image',
65
+ mime_type=mime_type,
66
+ data=data,
67
+ name=f"approval_image.{mime_type.split('/')[-1]}"
68
+ )
69
+ attachments.append(attachment)
70
+ except Exception as e:
71
+ print(f"[JAF:APPROVAL] Failed to process image_context: {e}")
72
+
73
+ return attachments
74
+
75
+
76
+ def _add_approval_context_to_conversation(
77
+ state: RunState[Any],
78
+ additional_context: Optional[Dict[str, Any]]
79
+ ) -> RunState[Any]:
80
+ """Add approval context including images to the conversation."""
81
+ if not additional_context:
82
+ return state
83
+
84
+ # Extract image attachments
85
+ attachments = _process_additional_context_images(additional_context)
86
+
87
+ if not attachments:
88
+ return state
89
+
90
+ # Create approval context message
91
+ approval_message = "Additional context provided during approval process."
92
+
93
+ # Check if there are text messages to include
94
+ messages = additional_context.get('messages', [])
95
+ if messages:
96
+ text_content = []
97
+ for msg in messages:
98
+ content = msg.get('content', '')
99
+ if content:
100
+ text_content.append(content)
101
+
102
+ if text_content:
103
+ approval_message = f"User provided additional context: {' '.join(text_content)}"
104
+
105
+ # Create user message with attachments (using USER role for better compatibility)
106
+ context_message = Message(
107
+ role=ContentRole.USER,
108
+ content=approval_message,
109
+ attachments=attachments
110
+ )
111
+
112
+ # Add to conversation
113
+ new_messages = state.messages + [context_message]
114
+ return replace(state, messages=new_messages)
12
115
 
13
116
 
14
117
  async def approve(
@@ -60,8 +163,12 @@ async def approve(
60
163
  # Update in-memory state
61
164
  new_approvals = {**state.approvals}
62
165
  new_approvals[interruption.tool_call.id] = approval_value
63
-
64
- return replace(state, approvals=new_approvals)
166
+
167
+ # Process any image context and add to conversation
168
+ updated_state = replace(state, approvals=new_approvals)
169
+ updated_state = _add_approval_context_to_conversation(updated_state, additional_context)
170
+
171
+ return updated_state
65
172
 
66
173
  return state
67
174
 
@@ -115,8 +222,12 @@ async def reject(
115
222
  # Update in-memory state
116
223
  new_approvals = {**state.approvals}
117
224
  new_approvals[interruption.tool_call.id] = approval_value
118
-
119
- return replace(state, approvals=new_approvals)
225
+
226
+ # Process any image context and add to conversation
227
+ updated_state = replace(state, approvals=new_approvals)
228
+ updated_state = _add_approval_context_to_conversation(updated_state, additional_context)
229
+
230
+ return updated_state
120
231
 
121
232
  return state
122
233
 
jaf/core/tracing.py CHANGED
@@ -7,6 +7,7 @@ tool calls, and performance metrics.
7
7
  import os
8
8
  os.environ["LANGFUSE_ENABLE_OTEL"] = "false"
9
9
  import json
10
+ import logging
10
11
  import time
11
12
  from datetime import datetime
12
13
  from typing import Any, Dict, List, Optional, Protocol
@@ -18,6 +19,12 @@ from opentelemetry.sdk.resources import Resource
18
19
  from opentelemetry.sdk.trace import TracerProvider
19
20
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
20
21
  from langfuse import Langfuse
22
+ import httpx
23
+
24
+ try:
25
+ import requests
26
+ except ImportError:
27
+ requests = None # type: ignore
21
28
 
22
29
  from .types import TraceEvent, TraceId
23
30
 
@@ -25,8 +32,25 @@ from .types import TraceEvent, TraceId
25
32
  provider = None
26
33
  tracer = None
27
34
 
28
- def setup_otel_tracing(service_name: str = "jaf-agent", collector_url: Optional[str] = None) -> None:
29
- """Configure OpenTelemetry tracing."""
35
+ def setup_otel_tracing(
36
+ service_name: str = "jaf-agent",
37
+ collector_url: Optional[str] = None,
38
+ proxy: Optional[str] = None,
39
+ session: Optional[Any] = None,
40
+ timeout: Optional[int] = None
41
+ ) -> None:
42
+ """Configure OpenTelemetry tracing.
43
+
44
+ Args:
45
+ service_name: Name of the service for tracing.
46
+ collector_url: OTLP collector endpoint URL.
47
+ proxy: Optional proxy URL (e.g., "http://proxy.example.com:8080").
48
+ Falls back to OTEL_PROXY environment variable.
49
+ If not provided, respects standard HTTP_PROXY/HTTPS_PROXY env vars.
50
+ session: Optional custom requests.Session for advanced configuration.
51
+ If provided, proxy parameter is ignored.
52
+ timeout: Optional timeout in seconds for OTLP requests.
53
+ """
30
54
  global provider, tracer
31
55
  if not collector_url:
32
56
  return
@@ -34,8 +58,38 @@ def setup_otel_tracing(service_name: str = "jaf-agent", collector_url: Optional[
34
58
  provider = TracerProvider(
35
59
  resource=Resource.create({"service.name": service_name})
36
60
  )
37
-
38
- exporter = OTLPSpanExporter(endpoint=collector_url)
61
+
62
+ # Configure session with proxy if needed
63
+ effective_session = session
64
+ # Configure session with proxy if needed
65
+ effective_session = session
66
+ if effective_session is None and requests is not None:
67
+ effective_proxy = proxy or os.environ.get("OTEL_PROXY")
68
+ if effective_proxy:
69
+ effective_session = requests.Session()
70
+ effective_session.proxies = {
71
+ 'http': effective_proxy,
72
+ 'https': effective_proxy,
73
+ }
74
+ print(f"[OTEL] Configuring proxy: {effective_proxy}")
75
+ elif effective_session is None and requests is None and (proxy or os.environ.get("OTEL_PROXY")):
76
+ print(f"[OTEL] Warning: Proxy configuration ignored - 'requests' package not installed")
77
+ if effective_proxy:
78
+ effective_session = requests.Session()
79
+ effective_session.proxies = {
80
+ 'http': effective_proxy,
81
+ 'https': effective_proxy,
82
+ }
83
+ print(f"[OTEL] Configuring proxy: {effective_proxy}")
84
+
85
+ # Create exporter with optional session and timeout
86
+ exporter_kwargs = {"endpoint": collector_url}
87
+ if effective_session is not None:
88
+ exporter_kwargs["session"] = effective_session
89
+ if timeout is not None:
90
+ exporter_kwargs["timeout"] = timeout
91
+
92
+ exporter = OTLPSpanExporter(**exporter_kwargs)
39
93
  provider.add_span_processor(BatchSpanProcessor(exporter))
40
94
  trace.set_tracer_provider(provider)
41
95
  tracer = trace.get_tracer(__name__)
@@ -326,29 +380,206 @@ class FileTraceCollector:
326
380
  self.in_memory.clear(trace_id)
327
381
 
328
382
  class LangfuseTraceCollector:
329
- """Langfuse trace collector using v2 SDK."""
383
+ """Langfuse trace collector using v2 SDK.
330
384
 
331
- def __init__(self):
385
+ Supports proxy configuration through:
386
+ 1. Custom httpx.Client via httpx_client parameter
387
+ 2. Proxy URL via proxy parameter or LANGFUSE_PROXY environment variable
388
+ 3. Standard HTTP_PROXY/HTTPS_PROXY environment variables (httpx respects these automatically)
389
+ """
390
+
391
+ def __init__(
392
+ self,
393
+ httpx_client: Optional[httpx.Client] = None,
394
+ proxy: Optional[str] = None,
395
+ timeout: Optional[int] = None
396
+ ):
397
+ """Initialize Langfuse trace collector.
398
+
399
+ Args:
400
+ httpx_client: Optional custom httpx.Client with proxy configuration.
401
+ If provided, this will be used for all API calls.
402
+ proxy: Optional proxy URL (e.g., "http://my.proxy.example.com:8080").
403
+ Only used if httpx_client is not provided.
404
+ Falls back to LANGFUSE_PROXY environment variable.
405
+ timeout: Optional timeout in seconds for HTTP requests. Defaults to 10.
406
+ """
332
407
  public_key = os.environ.get("LANGFUSE_PUBLIC_KEY")
333
408
  secret_key = os.environ.get("LANGFUSE_SECRET_KEY")
334
409
  host = os.environ.get("LANGFUSE_HOST")
335
-
410
+
336
411
  print(f"[LANGFUSE] Initializing with host: {host}")
337
412
  print(f"[LANGFUSE] Public key: {public_key[:10]}..." if public_key else "[LANGFUSE] No public key set")
338
413
  print(f"[LANGFUSE] Secret key: {secret_key[:10]}..." if secret_key else "[LANGFUSE] No secret key set")
339
-
414
+
415
+ # Track if we own the client for cleanup
416
+ self._owns_httpx_client = False
417
+
418
+ # Configure httpx client with proxy if needed
419
+ client = httpx_client
420
+ if client is None:
421
+ # Use provided proxy, environment variable, or None
422
+ effective_proxy = proxy or os.environ.get("LANGFUSE_PROXY")
423
+ effective_timeout = timeout or int(os.environ.get("LANGFUSE_TIMEOUT", "10"))
424
+
425
+ if effective_proxy:
426
+ print(f"[LANGFUSE] Configuring proxy: {effective_proxy}")
427
+ try:
428
+ client = httpx.Client(proxy=effective_proxy, timeout=effective_timeout)
429
+ self._owns_httpx_client = True
430
+ except httpx.InvalidURL as e:
431
+ logger = logging.getLogger(__name__)
432
+ logger.error(f"[LANGFUSE] Invalid proxy URL '{effective_proxy}': {e}")
433
+ raise
434
+ except Exception as e:
435
+ logger = logging.getLogger(__name__)
436
+ logger.error(f"[LANGFUSE] Failed to create httpx.Client with proxy '{effective_proxy}': {e}")
437
+ raise
438
+ # If no proxy specified, httpx will still respect HTTP_PROXY/HTTPS_PROXY env vars
439
+ elif proxy:
440
+ print(f"[LANGFUSE] Warning: proxy parameter ignored because httpx_client is provided")
441
+
340
442
  self.langfuse = Langfuse(
341
443
  public_key=public_key,
342
444
  secret_key=secret_key,
343
445
  host=host,
344
- release="jaf-py-v2.5.1"
446
+ release="jaf-py-v2.5.3",
447
+ httpx_client=client
345
448
  )
449
+ self._httpx_client = client
450
+
451
+ # Detect Langfuse version (v2 has trace() method, v3 does not)
452
+ self._is_langfuse_v3 = not hasattr(self.langfuse, 'trace')
453
+ if self._is_langfuse_v3:
454
+ print("[LANGFUSE] Detected Langfuse v3.x - using OpenTelemetry-based API")
455
+ else:
456
+ print("[LANGFUSE] Detected Langfuse v2.x - using legacy API")
457
+
346
458
  self.active_spans: Dict[str, Any] = {}
347
459
  self.trace_spans: Dict[TraceId, Any] = {}
348
460
  # Track tool calls and results for each trace
349
461
  self.trace_tool_calls: Dict[TraceId, List[Dict[str, Any]]] = {}
350
462
  self.trace_tool_results: Dict[TraceId, List[Dict[str, Any]]] = {}
351
463
 
464
+ def __del__(self) -> None:
465
+ """Cleanup resources on deletion."""
466
+ self.close()
467
+
468
+ def close(self) -> None:
469
+ """Close httpx client if we own it."""
470
+ if self._owns_httpx_client and self._httpx_client:
471
+ try:
472
+ self._httpx_client.close()
473
+ except Exception as e:
474
+ print(f"[LANGFUSE] Warning: Failed to close httpx client: {e}")
475
+
476
+ def _get_event_data(self, event: TraceEvent, key: str, default: Any = None) -> Any:
477
+ """Extract data from event, handling both dict and dataclass."""
478
+ if not hasattr(event, 'data'):
479
+ return default
480
+
481
+ # Handle dict
482
+ if isinstance(event.data, dict):
483
+ return event.data.get(key, default)
484
+
485
+ # Handle dataclass/object with attributes
486
+ return getattr(event.data, key, default)
487
+
488
+ def _create_trace(self, trace_id: TraceId, **kwargs) -> Any:
489
+ """Create a trace using the appropriate API for the Langfuse version."""
490
+ if self._is_langfuse_v3:
491
+ # Langfuse v3: Use start_span() to create a root span (creates trace implicitly)
492
+ # Extract parameters for v3 API
493
+ name = kwargs.get('name', 'trace')
494
+ input_data = kwargs.get('input')
495
+ metadata = kwargs.get('metadata', {})
496
+ user_id = kwargs.get('user_id')
497
+ session_id = kwargs.get('session_id')
498
+ tags = kwargs.get('tags', [])
499
+
500
+ # Add user_id, session_id, and tags to metadata for v3
501
+ if user_id:
502
+ metadata['user_id'] = user_id
503
+ if session_id:
504
+ metadata['session_id'] = session_id
505
+ if tags:
506
+ metadata['tags'] = tags
507
+
508
+ # Create root span
509
+ trace = self.langfuse.start_span(
510
+ name=name,
511
+ input=input_data,
512
+ metadata=metadata
513
+ )
514
+
515
+ # Update trace properties using update_trace()
516
+ update_params = {}
517
+ if user_id:
518
+ update_params['user_id'] = user_id
519
+ if session_id:
520
+ update_params['session_id'] = session_id
521
+ if tags:
522
+ update_params['tags'] = tags
523
+
524
+ if update_params:
525
+ trace.update_trace(**update_params)
526
+
527
+ return trace
528
+ else:
529
+ # Langfuse v2: Use trace() method
530
+ return self.langfuse.trace(**kwargs)
531
+
532
+ def _create_generation(self, parent_span: Any, **kwargs) -> Any:
533
+ """Create a generation using the appropriate API for the Langfuse version."""
534
+ if self._is_langfuse_v3:
535
+ # Langfuse v3: Use start_generation() method
536
+ return parent_span.start_generation(**kwargs)
537
+ else:
538
+ # Langfuse v2: Use generation() method
539
+ return parent_span.generation(**kwargs)
540
+
541
+ def _create_span(self, parent_span: Any, **kwargs) -> Any:
542
+ """Create a span using the appropriate API for the Langfuse version."""
543
+ if self._is_langfuse_v3:
544
+ # Langfuse v3: Use start_span() method
545
+ return parent_span.start_span(**kwargs)
546
+ else:
547
+ # Langfuse v2: Use span() method
548
+ return parent_span.span(**kwargs)
549
+
550
+ def _create_event(self, parent_span: Any, **kwargs) -> Any:
551
+ """Create an event using the appropriate API for the Langfuse version."""
552
+ if self._is_langfuse_v3:
553
+ # Langfuse v3: Use create_event() method
554
+ return parent_span.create_event(**kwargs)
555
+ else:
556
+ # Langfuse v2: Use event() method
557
+ return parent_span.event(**kwargs)
558
+
559
+ def _end_span(self, span: Any, **kwargs) -> None:
560
+ """End a span/generation using the appropriate API for the Langfuse version."""
561
+ if self._is_langfuse_v3:
562
+ # Langfuse v3: Call update() first with output/metadata, then end()
563
+ update_params = {}
564
+ end_params = {}
565
+
566
+ # Separate parameters for update() vs end()
567
+ for key, value in kwargs.items():
568
+ if key in ['output', 'metadata', 'model', 'usage']:
569
+ update_params[key] = value
570
+ elif key == 'end_time':
571
+ end_params[key] = value
572
+
573
+ # Update first if there are parameters
574
+ if update_params:
575
+ span.update(**update_params)
576
+
577
+ # Then end
578
+ span.end(**end_params)
579
+ else:
580
+ # Langfuse v2: Call end() directly with all parameters
581
+ span.end(**kwargs)
582
+
352
583
  def collect(self, event: TraceEvent) -> None:
353
584
  """Collect a trace event and send it to Langfuse."""
354
585
  try:
@@ -373,15 +604,15 @@ class LangfuseTraceCollector:
373
604
  conversation_history = []
374
605
 
375
606
  # Debug: Print the event data structure to understand what we're working with
376
- if event.data.get("context"):
377
- context = event.data["context"]
607
+ if self._get_event_data(event, "context"):
608
+ context = self._get_event_data(event, "context")
378
609
  print(f"[LANGFUSE DEBUG] Context type: {type(context)}")
379
610
  print(f"[LANGFUSE DEBUG] Context attributes: {dir(context) if hasattr(context, '__dict__') else 'Not an object'}")
380
611
  if hasattr(context, '__dict__'):
381
612
  print(f"[LANGFUSE DEBUG] Context dict: {context.__dict__}")
382
613
 
383
614
  # Try to extract from context first
384
- context = event.data.get("context")
615
+ context = self._get_event_data(event, "context")
385
616
  if context:
386
617
  # Try direct attribute access
387
618
  if hasattr(context, 'query'):
@@ -411,7 +642,7 @@ class LangfuseTraceCollector:
411
642
  print(f"[LANGFUSE DEBUG] Extracted user_id from attr: {user_id}")
412
643
 
413
644
  # Extract conversation history and current user query from messages
414
- messages = event.data.get("messages", [])
645
+ messages = self._get_event_data(event, "messages", [])
415
646
  if messages:
416
647
  print(f"[LANGFUSE DEBUG] Processing {len(messages)} messages")
417
648
 
@@ -503,20 +734,22 @@ class LangfuseTraceCollector:
503
734
  trace_input = {
504
735
  "user_query": user_query,
505
736
  "run_id": str(trace_id),
506
- "agent_name": event.data.get("agent_name", "analytics_agent_jaf"),
737
+ "agent_name": self._get_event_data(event, "agent_name", "analytics_agent_jaf"),
507
738
  "session_info": {
508
- "session_id": event.data.get("session_id"),
509
- "user_id": user_id or event.data.get("user_id")
739
+ "session_id": self._get_event_data(event, "session_id"),
740
+ "user_id": user_id or self._get_event_data(event, "user_id")
510
741
  }
511
742
  }
512
743
 
513
744
  # Extract agent_name for tagging
514
- agent_name = event.data.get("agent_name") or "analytics_agent_jaf"
745
+ agent_name = self._get_event_data(event, "agent_name") or "analytics_agent_jaf"
515
746
 
516
- trace = self.langfuse.trace(
747
+ # Use compatibility layer to create trace (works with both v2 and v3)
748
+ trace = self._create_trace(
749
+ trace_id=trace_id,
517
750
  name=agent_name,
518
- user_id=user_id or event.data.get("user_id"),
519
- session_id=event.data.get("session_id"),
751
+ user_id=user_id or self._get_event_data(event, "user_id"),
752
+ session_id=self._get_event_data(event, "session_id"),
520
753
  input=trace_input,
521
754
  tags=[agent_name], # Add agent_name as a tag for dashboard filtering
522
755
  metadata={
@@ -524,17 +757,17 @@ class LangfuseTraceCollector:
524
757
  "event_type": "run_start",
525
758
  "trace_id": str(trace_id),
526
759
  "user_query": user_query,
527
- "user_id": user_id or event.data.get("user_id"),
760
+ "user_id": user_id or self._get_event_data(event, "user_id"),
528
761
  "agent_name": agent_name,
529
762
  "conversation_history": conversation_history,
530
763
  "tool_calls": [],
531
764
  "tool_results": [],
532
- "user_info": event.data.get("context").user_info if event.data.get("context") and hasattr(event.data.get("context"), 'user_info') else None
765
+ "user_info": self._get_event_data(event, "context").user_info if self._get_event_data(event, "context") and hasattr(self._get_event_data(event, "context"), 'user_info') else None
533
766
  }
534
767
  )
535
768
  self.trace_spans[trace_id] = trace
536
769
  # Store user_id, user_query, and conversation_history for later use
537
- trace._user_id = user_id or event.data.get("user_id")
770
+ trace._user_id = user_id or self._get_event_data(event, "user_id")
538
771
  trace._user_query = user_query
539
772
  trace._conversation_history = conversation_history
540
773
  print(f"[LANGFUSE] Created trace with user query: {user_query[:100] if user_query else 'None'}...")
@@ -551,7 +784,7 @@ class LangfuseTraceCollector:
551
784
  "trace_id": str(trace_id),
552
785
  "user_query": getattr(self.trace_spans[trace_id], '_user_query', None),
553
786
  "user_id": getattr(self.trace_spans[trace_id], '_user_id', None),
554
- "agent_name": event.data.get("agent_name", "analytics_agent_jaf"),
787
+ "agent_name": self._get_event_data(event, "agent_name", "analytics_agent_jaf"),
555
788
  "conversation_history": conversation_history,
556
789
  "tool_calls": self.trace_tool_calls.get(trace_id, []),
557
790
  "tool_results": self.trace_tool_results.get(trace_id, [])
@@ -579,7 +812,7 @@ class LangfuseTraceCollector:
579
812
 
580
813
  elif event.type == "llm_call_start":
581
814
  # Start a generation for LLM calls
582
- model = event.data.get("model", "unknown")
815
+ model = self._get_event_data(event, "model", "unknown")
583
816
  print(f"[LANGFUSE] Starting generation for LLM call with model: {model}")
584
817
 
585
818
  # Get stored user information from the trace
@@ -587,11 +820,13 @@ class LangfuseTraceCollector:
587
820
  user_id = getattr(trace, '_user_id', None)
588
821
  user_query = getattr(trace, '_user_query', None)
589
822
 
590
- generation = trace.generation(
823
+ # Use compatibility layer to create generation (works with both v2 and v3)
824
+ generation = self._create_generation(
825
+ parent_span=trace,
591
826
  name=f"llm-call-{model}",
592
- input=event.data.get("messages"),
827
+ input=self._get_event_data(event, "messages"),
593
828
  metadata={
594
- "agent_name": event.data.get("agent_name"),
829
+ "agent_name": self._get_event_data(event, "agent_name"),
595
830
  "model": model,
596
831
  "user_id": user_id,
597
832
  "user_query": user_query
@@ -607,10 +842,10 @@ class LangfuseTraceCollector:
607
842
  print(f"[LANGFUSE] Ending generation for LLM call")
608
843
  # End the generation
609
844
  generation = self.active_spans[span_id]
610
- choice = event.data.get("choice", {})
611
-
845
+ choice = self._get_event_data(event, "choice", {})
846
+
612
847
  # Extract usage from the event data
613
- usage = event.data.get("usage", {})
848
+ usage = self._get_event_data(event, "usage", {})
614
849
 
615
850
  # Extract model information from choice data or event data
616
851
  model = choice.get("model", "unknown")
@@ -636,8 +871,10 @@ class LangfuseTraceCollector:
636
871
  print(f"[LANGFUSE] Usage data for automatic cost calculation: {langfuse_usage}")
637
872
 
638
873
  # Include model information in the generation end - Langfuse will calculate costs automatically
639
- generation.end(
640
- output=choice,
874
+ # Use compatibility wrapper for ending spans/generations
875
+ self._end_span(
876
+ span=generation,
877
+ output=choice,
641
878
  usage=langfuse_usage,
642
879
  model=model, # Pass model directly for automatic cost calculation
643
880
  metadata={
@@ -656,9 +893,9 @@ class LangfuseTraceCollector:
656
893
 
657
894
  elif event.type == "tool_call_start":
658
895
  # Start a span for tool calls with detailed input information
659
- tool_name = event.data.get('tool_name', 'unknown')
660
- tool_args = event.data.get("args", {})
661
- call_id = event.data.get("call_id")
896
+ tool_name = self._get_event_data(event, 'tool_name', 'unknown')
897
+ tool_args = self._get_event_data(event, "args", {})
898
+ call_id = self._get_event_data(event, "call_id")
662
899
  if not call_id:
663
900
  call_id = f"{tool_name}-{uuid.uuid4().hex[:8]}"
664
901
  try:
@@ -691,7 +928,9 @@ class LangfuseTraceCollector:
691
928
  "timestamp": datetime.now().isoformat()
692
929
  }
693
930
 
694
- span = self.trace_spans[trace_id].span(
931
+ # Use compatibility layer to create span (works with both v2 and v3)
932
+ span = self._create_span(
933
+ parent_span=self.trace_spans[trace_id],
695
934
  name=f"tool-{tool_name}",
696
935
  input=tool_input,
697
936
  metadata={
@@ -708,9 +947,9 @@ class LangfuseTraceCollector:
708
947
  elif event.type == "tool_call_end":
709
948
  span_id = self._get_span_id(event)
710
949
  if span_id in self.active_spans:
711
- tool_name = event.data.get('tool_name', 'unknown')
712
- tool_result = event.data.get("result")
713
- call_id = event.data.get("call_id")
950
+ tool_name = self._get_event_data(event, 'tool_name', 'unknown')
951
+ tool_result = self._get_event_data(event, "result")
952
+ call_id = self._get_event_data(event, "call_id")
714
953
 
715
954
  print(f"[LANGFUSE] Ending span for tool call: {tool_name} ({call_id})")
716
955
 
@@ -720,9 +959,9 @@ class LangfuseTraceCollector:
720
959
  "result": tool_result,
721
960
  "call_id": call_id,
722
961
  "timestamp": datetime.now().isoformat(),
723
- "execution_status": event.data.get("execution_status", "completed"),
724
- "status": event.data.get("execution_status", "completed"), # DEPRECATED: backward compatibility
725
- "tool_result": event.data.get("tool_result")
962
+ "execution_status": self._get_event_data(event, "execution_status", "completed"),
963
+ "status": self._get_event_data(event, "execution_status", "completed"), # DEPRECATED: backward compatibility
964
+ "tool_result": self._get_event_data(event, "tool_result")
726
965
  }
727
966
 
728
967
  if trace_id not in self.trace_tool_results:
@@ -736,13 +975,15 @@ class LangfuseTraceCollector:
736
975
  "result": tool_result,
737
976
  "call_id": call_id,
738
977
  "timestamp": datetime.now().isoformat(),
739
- "execution_status": event.data.get("execution_status", "completed"),
740
- "status": event.data.get("execution_status", "completed") # DEPRECATED: backward compatibility
978
+ "execution_status": self._get_event_data(event, "execution_status", "completed"),
979
+ "status": self._get_event_data(event, "execution_status", "completed") # DEPRECATED: backward compatibility
741
980
  }
742
981
 
743
982
  # End the span with detailed output
983
+ # Use compatibility wrapper for ending spans/generations
744
984
  span = self.active_spans[span_id]
745
- span.end(
985
+ self._end_span(
986
+ span=span,
746
987
  output=tool_output,
747
988
  metadata={
748
989
  "tool_name": tool_name,
@@ -762,17 +1003,21 @@ class LangfuseTraceCollector:
762
1003
  elif event.type == "handoff":
763
1004
  # Create an event for handoffs
764
1005
  print(f"[LANGFUSE] Creating event for handoff")
765
- self.trace_spans[trace_id].event(
1006
+ # Use compatibility layer to create event (works with both v2 and v3)
1007
+ self._create_event(
1008
+ parent_span=self.trace_spans[trace_id],
766
1009
  name="agent-handoff",
767
- input={"from": event.data.get("from"), "to": event.data.get("to")},
1010
+ input={"from": self._get_event_data(event, "from"), "to": self._get_event_data(event, "to")},
768
1011
  metadata=event.data
769
1012
  )
770
1013
  print(f"[LANGFUSE] Handoff event created")
771
-
1014
+
772
1015
  else:
773
1016
  # Create a generic event for other event types
774
1017
  print(f"[LANGFUSE] Creating generic event for: {event.type}")
775
- self.trace_spans[trace_id].event(
1018
+ # Use compatibility layer to create event (works with both v2 and v3)
1019
+ self._create_event(
1020
+ parent_span=self.trace_spans[trace_id],
776
1021
  name=event.type,
777
1022
  input=event.data,
778
1023
  metadata={"framework": "jaf", "event_type": event.type}
@@ -786,20 +1031,28 @@ class LangfuseTraceCollector:
786
1031
  traceback.print_exc()
787
1032
 
788
1033
  def _get_trace_id(self, event: TraceEvent) -> Optional[TraceId]:
789
- """Extract trace ID from event data."""
790
- if hasattr(event, 'data') and isinstance(event.data, dict):
791
- # Try snake_case first (Python convention)
792
- if 'trace_id' in event.data:
793
- return event.data['trace_id']
794
- elif 'run_id' in event.data:
795
- return TraceId(event.data['run_id'])
796
- # Fallback to camelCase (for compatibility)
797
- elif 'traceId' in event.data:
798
- return event.data['traceId']
799
- elif 'runId' in event.data:
800
- return TraceId(event.data['runId'])
801
-
802
- # Debug: print what's actually in the event data
1034
+ """Extract trace ID from event data, handling both dict and dataclass."""
1035
+ if not hasattr(event, 'data'):
1036
+ return None
1037
+
1038
+ # Try snake_case first (Python convention)
1039
+ trace_id = self._get_event_data(event, 'trace_id')
1040
+ if trace_id:
1041
+ return trace_id
1042
+
1043
+ run_id = self._get_event_data(event, 'run_id')
1044
+ if run_id:
1045
+ return TraceId(run_id)
1046
+
1047
+ # Fallback to camelCase (for compatibility)
1048
+ trace_id = self._get_event_data(event, 'traceId')
1049
+ if trace_id:
1050
+ return trace_id
1051
+
1052
+ run_id = self._get_event_data(event, 'runId')
1053
+ if run_id:
1054
+ return TraceId(run_id)
1055
+
803
1056
  return None
804
1057
 
805
1058
  def _get_span_id(self, event: TraceEvent) -> str:
@@ -808,15 +1061,15 @@ class LangfuseTraceCollector:
808
1061
 
809
1062
  # Use consistent identifiers that don't depend on timestamp
810
1063
  if event.type.startswith('tool_call'):
811
- call_id = event.data.get('call_id') or event.data.get('tool_call_id')
1064
+ call_id = self._get_event_data(event, 'call_id') or self._get_event_data(event, 'tool_call_id')
812
1065
  if call_id:
813
1066
  return f"tool-{trace_id}-{call_id}"
814
- tool_name = event.data.get('tool_name') or event.data.get('toolName', 'unknown')
1067
+ tool_name = self._get_event_data(event, 'tool_name') or self._get_event_data(event, 'toolName', 'unknown')
815
1068
  return f"tool-{tool_name}-{trace_id}"
816
1069
  elif event.type.startswith('llm_call'):
817
1070
  # For LLM calls, use a simpler consistent ID that matches between start and end
818
1071
  # Get run_id for more consistent matching
819
- run_id = event.data.get('run_id') or event.data.get('runId', trace_id)
1072
+ run_id = self._get_event_data(event, 'run_id') or self._get_event_data(event, 'runId', trace_id)
820
1073
  return f"llm-{run_id}"
821
1074
  else:
822
1075
  return f"{event.type}-{trace_id}"
@@ -834,21 +1087,50 @@ class LangfuseTraceCollector:
834
1087
  pass
835
1088
 
836
1089
 
837
- def create_composite_trace_collector(*collectors: TraceCollector) -> TraceCollector:
838
- """Create a composite trace collector that forwards events to multiple collectors."""
839
-
1090
+ def create_composite_trace_collector(
1091
+ *collectors: TraceCollector,
1092
+ httpx_client: Optional[httpx.Client] = None,
1093
+ otel_session: Optional[Any] = None,
1094
+ proxy: Optional[str] = None,
1095
+ timeout: Optional[int] = None
1096
+ ) -> TraceCollector:
1097
+ """Create a composite trace collector that forwards events to multiple collectors.
1098
+
1099
+ Args:
1100
+ *collectors: Variable length list of trace collectors
1101
+ httpx_client: Optional custom httpx.Client for Langfuse API calls.
1102
+ If provided, proxy and timeout parameters are ignored for Langfuse.
1103
+ otel_session: Optional custom requests.Session for OTLP HTTP calls.
1104
+ If provided, proxy parameter is ignored for OTEL.
1105
+ proxy: Optional proxy URL for both Langfuse and OTEL (e.g., "http://proxy.example.com:8080").
1106
+ For Langfuse: Falls back to LANGFUSE_PROXY environment variable.
1107
+ For OTEL: Falls back to OTEL_PROXY environment variable.
1108
+ If not set, both respect standard HTTP_PROXY/HTTPS_PROXY environment variables.
1109
+ timeout: Optional timeout in seconds for HTTP requests (applies to both Langfuse and OTEL).
1110
+ For Langfuse: Falls back to LANGFUSE_TIMEOUT environment variable (default: 10).
1111
+ """
840
1112
  collector_list = list(collectors)
841
-
1113
+
842
1114
  # Automatically add OTEL collector if URL is configured
843
1115
  collector_url = os.getenv("TRACE_COLLECTOR_URL")
844
1116
  if collector_url:
845
- setup_otel_tracing(collector_url=collector_url)
1117
+ # Pass proxy and timeout to OTEL setup
1118
+ setup_otel_tracing(
1119
+ collector_url=collector_url,
1120
+ proxy=proxy,
1121
+ session=otel_session,
1122
+ timeout=timeout
1123
+ )
846
1124
  otel_collector = OtelTraceCollector()
847
1125
  collector_list.append(otel_collector)
848
1126
 
849
1127
  # Automatically add Langfuse collector if keys are configured
850
1128
  if os.getenv("LANGFUSE_PUBLIC_KEY") and os.getenv("LANGFUSE_SECRET_KEY"):
851
- langfuse_collector = LangfuseTraceCollector()
1129
+ langfuse_collector = LangfuseTraceCollector(
1130
+ httpx_client=httpx_client,
1131
+ proxy=proxy,
1132
+ timeout=timeout
1133
+ )
852
1134
  collector_list.append(langfuse_collector)
853
1135
 
854
1136
  class CompositeTraceCollector:
@@ -882,5 +1164,21 @@ def create_composite_trace_collector(*collectors: TraceCollector) -> TraceCollec
882
1164
  collector.clear(trace_id)
883
1165
  except Exception as e:
884
1166
  print(f"Warning: Failed to clear trace collector: {e}")
1167
+
1168
+ def close(self) -> None:
1169
+ """Close all collectors that support cleanup."""
1170
+ for collector in self.collectors:
1171
+ if hasattr(collector, 'close'):
1172
+ try:
1173
+ collector.close()
1174
+ except Exception as e:
1175
+ print(f"Warning: Failed to close trace collector: {e}")
1176
+
1177
+ def __enter__(self):
1178
+ return self
1179
+
1180
+ def __exit__(self, exc_type, exc_val, exc_tb):
1181
+ self.close()
1182
+ return False
885
1183
 
886
1184
  return CompositeTraceCollector(collector_list)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jaf-py
3
- Version: 2.5.1
3
+ Version: 2.5.3
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
@@ -82,7 +82,7 @@ Dynamic: license-file
82
82
 
83
83
  <!-- ![JAF Banner](docs/cover.png) -->
84
84
 
85
- [![Version](https://img.shields.io/badge/version-2.3.1-blue.svg)](https://github.com/xynehq/jaf-py)
85
+ [![Version](https://img.shields.io/badge/version-2.5.3-blue.svg)](https://github.com/xynehq/jaf-py)
86
86
  [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
87
87
  [![Docs](https://img.shields.io/badge/Docs-Live-brightgreen)](https://xynehq.github.io/jaf-py/)
88
88
 
@@ -1,4 +1,4 @@
1
- jaf/__init__.py,sha256=kNxEp8Gr_lmJheZWRbjukSFjJX9K7RBOzAzTM690N7Y,8260
1
+ jaf/__init__.py,sha256=M7o--eb6Yp44HXoxq03cz650o8gY0ehAicf5J7d7nzg,8260
2
2
  jaf/cli.py,sha256=Af4di_NZ7rZ4wFl0R4EZh611NgJ--TL03vNyZ2M1_FY,8477
3
3
  jaf/exceptions.py,sha256=nl8JY355u7oTXB3PmC_LhnUaL8fzk2K4EaWM4fVpMPE,9196
4
4
  jaf/a2a/__init__.py,sha256=p4YVthZH0ow1ZECqWTQ0aQl8JWySYZb25jlzZJ09na4,7662
@@ -38,22 +38,23 @@ jaf/a2a/tests/test_client.py,sha256=L5h7DtQRVlULiRhRLtrmaCoYdvmbXsgLTy3QQ6KgmNM,
38
38
  jaf/a2a/tests/test_integration.py,sha256=I7LdgwN99mAOljM9kYtK7dGMMntTSWKMw_oLOcJjinU,18454
39
39
  jaf/a2a/tests/test_protocol.py,sha256=He3vGlBfIazpppAnuSybutrvjIN3VGxEleAohrVd9hc,23287
40
40
  jaf/a2a/tests/test_types.py,sha256=PgRjDVJrHSXuu05z0B5lsSUUY5qEdQLFJbLBIExyVgI,18384
41
- jaf/core/__init__.py,sha256=PIGKm8n6OQ8jcXRS0Hn3_Zsl8m2qX91N80YJoLCJ4eU,1762
41
+ jaf/core/__init__.py,sha256=1VHV2-a1oJXIWcg8n5G5g2cmjw2QXv7OezncNB59KLw,1988
42
42
  jaf/core/agent_tool.py,sha256=tfLNaTIcOZ0dR9GBP1AHLPkLExm_dLbURnVIN4R84FQ,11806
43
43
  jaf/core/analytics.py,sha256=zFHIWqWal0bbEFCmJDc4DKeM0Ja7b_D19PqVaBI12pA,23338
44
44
  jaf/core/composition.py,sha256=IVxRO1Q9nK7JRH32qQ4p8WMIUu66BhqPNrlTNMGFVwE,26317
45
45
  jaf/core/engine.py,sha256=wt5RW8ntEV8Prq5IoNiTIqAe6PBZM4XLTXYrwCUONTo,58252
46
46
  jaf/core/errors.py,sha256=5fwTNhkojKRQ4wZj3lZlgDnAsrYyjYOwXJkIr5EGNUc,5539
47
47
  jaf/core/guardrails.py,sha256=nv7pQuCx7-9DDZrecWO1DsDqFoujL81FBDrafOsXgcI,26179
48
+ jaf/core/handoff.py,sha256=ttjOQ6CSl34J4T_1ejdmq78gZ-ve07_IQE_DAbz2bmo,6002
48
49
  jaf/core/parallel_agents.py,sha256=ahwYoTnkrF4xQgV-hjc5sUaWhQWQFENMZG5riNa_Ieg,12165
49
50
  jaf/core/performance.py,sha256=jedQmTEkrKMD6_Aw1h8PdG-5TsdYSFFT7Or6k5dmN2g,9974
50
51
  jaf/core/proxy.py,sha256=_WM3cpRlSQLYpgSBrnY30UPMe2iZtlqDQ65kppE-WY0,4609
51
52
  jaf/core/proxy_helpers.py,sha256=i7a5fAX9rLmO4FMBX51-yRkTFwfWedzQNgnLmeLUd_A,4370
52
- jaf/core/state.py,sha256=NMtYTpUYa64m1Kte6lD8LGnF2bl69HAcdgXH6f-M97c,5650
53
+ jaf/core/state.py,sha256=oNCVXPWLkqnBQObdQX10TcmZ0eOF3wKG6DtL3kF6ohw,9649
53
54
  jaf/core/streaming.py,sha256=h_lYHQA9ee_D5QsDO9-Vhevgi7rFXPslPzd9605AJGo,17034
54
55
  jaf/core/tool_results.py,sha256=-bTOqOX02lMyslp5Z4Dmuhx0cLd5o7kgR88qK2HO_sw,11323
55
56
  jaf/core/tools.py,sha256=84N9A7QQ3xxcOs2eUUot3nmCnt5i7iZT9VwkuzuFBxQ,16274
56
- jaf/core/tracing.py,sha256=5-rQQdBlBHzP5tdwclWfl0Qt5Zh-9WvqNiDoPk6-y1g,40706
57
+ jaf/core/tracing.py,sha256=kY6qCVY2YQWWcbjU82icEdysdOEwYdoYfyM5I2YcSu4,53367
57
58
  jaf/core/types.py,sha256=GE7Q2s6jglAFfkpc1MIgVkC9XWaWT46OOQd7L17ACxw,27955
58
59
  jaf/core/workflows.py,sha256=Ul-82gzjIXtkhnSMSPv-8igikjkMtW1EBo9yrfodtvI,26294
59
60
  jaf/memory/__init__.py,sha256=-L98xlvihurGAzF0DnXtkueDVvO_wV2XxxEwAWdAj50,1400
@@ -86,9 +87,9 @@ jaf/visualization/functional_core.py,sha256=zedMDZbvjuOugWwnh6SJ2stvRNQX1Hlkb9Ab
86
87
  jaf/visualization/graphviz.py,sha256=WTOM6UP72-lVKwI4_SAr5-GCC3ouckxHv88ypCDQWJ0,12056
87
88
  jaf/visualization/imperative_shell.py,sha256=GpMrAlMnLo2IQgyB2nardCz09vMvAzaYI46MyrvJ0i4,2593
88
89
  jaf/visualization/types.py,sha256=QQcbVeQJLuAOXk8ynd08DXIS-PVCnv3R-XVE9iAcglw,1389
89
- jaf_py-2.5.1.dist-info/licenses/LICENSE,sha256=LXUQBJxdyr-7C4bk9cQBwvsF_xwA-UVstDTKabpcjlI,1063
90
- jaf_py-2.5.1.dist-info/METADATA,sha256=SyqK3LoYF86q-MfOa7qotPlQ8kBscVX9hWLO9i3U3fY,27743
91
- jaf_py-2.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
92
- jaf_py-2.5.1.dist-info/entry_points.txt,sha256=OtIJeNJpb24kgGrqRx9szGgDx1vL9ayq8uHErmu7U5w,41
93
- jaf_py-2.5.1.dist-info/top_level.txt,sha256=Xu1RZbGaM4_yQX7bpalo881hg7N_dybaOW282F15ruE,4
94
- jaf_py-2.5.1.dist-info/RECORD,,
90
+ jaf_py-2.5.3.dist-info/licenses/LICENSE,sha256=LXUQBJxdyr-7C4bk9cQBwvsF_xwA-UVstDTKabpcjlI,1063
91
+ jaf_py-2.5.3.dist-info/METADATA,sha256=tKpfginwaYb7zT0aUGt8a-cJ3mm8E5KfvWCLnWhYGGY,27743
92
+ jaf_py-2.5.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
93
+ jaf_py-2.5.3.dist-info/entry_points.txt,sha256=OtIJeNJpb24kgGrqRx9szGgDx1vL9ayq8uHErmu7U5w,41
94
+ jaf_py-2.5.3.dist-info/top_level.txt,sha256=Xu1RZbGaM4_yQX7bpalo881hg7N_dybaOW282F15ruE,4
95
+ jaf_py-2.5.3.dist-info/RECORD,,
File without changes