jaf-py 2.2.4__py3-none-any.whl → 2.3.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 +6 -2
- jaf/core/tracing.py +186 -38
- jaf/core/types.py +4 -0
- {jaf_py-2.2.4.dist-info → jaf_py-2.3.1.dist-info}/METADATA +1 -1
- {jaf_py-2.2.4.dist-info → jaf_py-2.3.1.dist-info}/RECORD +9 -9
- {jaf_py-2.2.4.dist-info → jaf_py-2.3.1.dist-info}/WHEEL +0 -0
- {jaf_py-2.2.4.dist-info → jaf_py-2.3.1.dist-info}/entry_points.txt +0 -0
- {jaf_py-2.2.4.dist-info → jaf_py-2.3.1.dist-info}/licenses/LICENSE +0 -0
- {jaf_py-2.2.4.dist-info → jaf_py-2.3.1.dist-info}/top_level.txt +0 -0
jaf/core/engine.py
CHANGED
|
@@ -94,7 +94,9 @@ async def run(
|
|
|
94
94
|
config.on_event(RunStartEvent(data=to_event_data(RunStartEventData(
|
|
95
95
|
run_id=initial_state.run_id,
|
|
96
96
|
trace_id=initial_state.trace_id,
|
|
97
|
-
session_id=config.conversation_id
|
|
97
|
+
session_id=config.conversation_id,
|
|
98
|
+
context=initial_state.context,
|
|
99
|
+
messages=initial_state.messages
|
|
98
100
|
))))
|
|
99
101
|
|
|
100
102
|
state_with_memory = await _load_conversation_history(initial_state, config)
|
|
@@ -293,7 +295,9 @@ async def _run_internal(
|
|
|
293
295
|
agent_name=current_agent.name,
|
|
294
296
|
model=model,
|
|
295
297
|
trace_id=state.trace_id,
|
|
296
|
-
run_id=state.run_id
|
|
298
|
+
run_id=state.run_id,
|
|
299
|
+
context=state.context,
|
|
300
|
+
messages=state.messages
|
|
297
301
|
))))
|
|
298
302
|
|
|
299
303
|
# Get completion from model provider
|
jaf/core/tracing.py
CHANGED
|
@@ -358,15 +358,98 @@ class LangfuseTraceCollector:
|
|
|
358
358
|
if event.type == "run_start":
|
|
359
359
|
# Start a new trace for the entire run
|
|
360
360
|
print(f"[LANGFUSE] Starting trace for run: {trace_id}")
|
|
361
|
+
|
|
362
|
+
# Extract user query from the run_start data
|
|
363
|
+
user_query = None
|
|
364
|
+
user_id = None
|
|
365
|
+
|
|
366
|
+
# Debug: Print the event data structure to understand what we're working with
|
|
367
|
+
if event.data.get("context"):
|
|
368
|
+
context = event.data["context"]
|
|
369
|
+
print(f"[LANGFUSE DEBUG] Context type: {type(context)}")
|
|
370
|
+
print(f"[LANGFUSE DEBUG] Context attributes: {dir(context) if hasattr(context, '__dict__') else 'Not an object'}")
|
|
371
|
+
if hasattr(context, '__dict__'):
|
|
372
|
+
print(f"[LANGFUSE DEBUG] Context dict: {context.__dict__}")
|
|
373
|
+
|
|
374
|
+
# Try to extract from context first
|
|
375
|
+
context = event.data.get("context")
|
|
376
|
+
if context:
|
|
377
|
+
# Try direct attribute access
|
|
378
|
+
if hasattr(context, 'query'):
|
|
379
|
+
user_query = context.query
|
|
380
|
+
print(f"[LANGFUSE DEBUG] Found user_query from context.query: {user_query}")
|
|
381
|
+
|
|
382
|
+
# Try to extract from combined_history
|
|
383
|
+
if hasattr(context, 'combined_history') and context.combined_history:
|
|
384
|
+
history = context.combined_history
|
|
385
|
+
print(f"[LANGFUSE DEBUG] Found combined_history with {len(history)} messages")
|
|
386
|
+
for i, msg in enumerate(reversed(history)):
|
|
387
|
+
print(f"[LANGFUSE DEBUG] History message {i}: {msg}")
|
|
388
|
+
if isinstance(msg, dict) and msg.get("role") == "user":
|
|
389
|
+
user_query = msg.get("content", "")
|
|
390
|
+
print(f"[LANGFUSE DEBUG] Found user_query from history: {user_query}")
|
|
391
|
+
break
|
|
392
|
+
|
|
393
|
+
# Try to extract user_id from token_response
|
|
394
|
+
if hasattr(context, 'token_response'):
|
|
395
|
+
token_response = context.token_response
|
|
396
|
+
print(f"[LANGFUSE DEBUG] Found token_response: {type(token_response)}")
|
|
397
|
+
if isinstance(token_response, dict):
|
|
398
|
+
user_id = token_response.get("email") or token_response.get("username")
|
|
399
|
+
print(f"[LANGFUSE DEBUG] Extracted user_id: {user_id}")
|
|
400
|
+
elif hasattr(token_response, 'email'):
|
|
401
|
+
user_id = token_response.email
|
|
402
|
+
print(f"[LANGFUSE DEBUG] Extracted user_id from attr: {user_id}")
|
|
403
|
+
|
|
404
|
+
# Fallback: try to extract from messages if context didn't work
|
|
405
|
+
if not user_query and event.data.get("messages"):
|
|
406
|
+
print(f"[LANGFUSE DEBUG] Trying fallback from messages")
|
|
407
|
+
messages = event.data["messages"]
|
|
408
|
+
print(f"[LANGFUSE DEBUG] Found {len(messages)} messages")
|
|
409
|
+
# Find the last user message which should be the current query
|
|
410
|
+
for i, msg in enumerate(reversed(messages)):
|
|
411
|
+
print(f"[LANGFUSE DEBUG] Message {i}: {msg}")
|
|
412
|
+
if isinstance(msg, dict) and msg.get("role") == "user":
|
|
413
|
+
user_query = msg.get("content", "")
|
|
414
|
+
print(f"[LANGFUSE DEBUG] Found user_query from messages: {user_query}")
|
|
415
|
+
break
|
|
416
|
+
elif hasattr(msg, 'role') and msg.role == 'user':
|
|
417
|
+
user_query = msg.content
|
|
418
|
+
print(f"[LANGFUSE DEBUG] Found user_query from message attr: {user_query}")
|
|
419
|
+
break
|
|
420
|
+
|
|
421
|
+
print(f"[LANGFUSE DEBUG] Final extracted - user_query: {user_query}, user_id: {user_id}")
|
|
422
|
+
|
|
423
|
+
# Create comprehensive input data for the trace
|
|
424
|
+
trace_input = {
|
|
425
|
+
"user_query": user_query,
|
|
426
|
+
"run_id": str(trace_id),
|
|
427
|
+
"agent_name": event.data.get("agent_name", "analytics_agent_jaf"),
|
|
428
|
+
"session_info": {
|
|
429
|
+
"session_id": event.data.get("session_id"),
|
|
430
|
+
"user_id": user_id or event.data.get("user_id")
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
361
434
|
trace = self.langfuse.trace(
|
|
362
435
|
name=f"jaf-run-{trace_id}",
|
|
363
|
-
user_id=event.data.get("user_id"),
|
|
436
|
+
user_id=user_id or event.data.get("user_id"),
|
|
364
437
|
session_id=event.data.get("session_id"),
|
|
365
|
-
input=
|
|
366
|
-
metadata={
|
|
438
|
+
input=trace_input,
|
|
439
|
+
metadata={
|
|
440
|
+
"framework": "jaf",
|
|
441
|
+
"event_type": "run_start",
|
|
442
|
+
"trace_id": str(trace_id),
|
|
443
|
+
"user_query": user_query,
|
|
444
|
+
"user_id": user_id or event.data.get("user_id"),
|
|
445
|
+
"agent_name": event.data.get("agent_name", "analytics_agent_jaf")
|
|
446
|
+
}
|
|
367
447
|
)
|
|
368
448
|
self.trace_spans[trace_id] = trace
|
|
369
|
-
|
|
449
|
+
# Store user_id and user_query for later use in generations
|
|
450
|
+
trace._user_id = user_id or event.data.get("user_id")
|
|
451
|
+
trace._user_query = user_query
|
|
452
|
+
print(f"[LANGFUSE] Created trace with user query: {user_query[:100] if user_query else 'None'}...")
|
|
370
453
|
|
|
371
454
|
elif event.type == "run_end":
|
|
372
455
|
if trace_id in self.trace_spans:
|
|
@@ -386,10 +469,21 @@ class LangfuseTraceCollector:
|
|
|
386
469
|
# Start a generation for LLM calls
|
|
387
470
|
model = event.data.get("model", "unknown")
|
|
388
471
|
print(f"[LANGFUSE] Starting generation for LLM call with model: {model}")
|
|
389
|
-
|
|
472
|
+
|
|
473
|
+
# Get stored user information from the trace
|
|
474
|
+
trace = self.trace_spans[trace_id]
|
|
475
|
+
user_id = getattr(trace, '_user_id', None)
|
|
476
|
+
user_query = getattr(trace, '_user_query', None)
|
|
477
|
+
|
|
478
|
+
generation = trace.generation(
|
|
390
479
|
name=f"llm-call-{model}",
|
|
391
480
|
input=event.data.get("messages"),
|
|
392
|
-
metadata={
|
|
481
|
+
metadata={
|
|
482
|
+
"agent_name": event.data.get("agent_name"),
|
|
483
|
+
"model": model,
|
|
484
|
+
"user_id": user_id,
|
|
485
|
+
"user_query": user_query
|
|
486
|
+
}
|
|
393
487
|
)
|
|
394
488
|
span_id = self._get_span_id(event)
|
|
395
489
|
self.active_spans[span_id] = generation
|
|
@@ -405,47 +499,111 @@ class LangfuseTraceCollector:
|
|
|
405
499
|
|
|
406
500
|
# Extract usage from the event data
|
|
407
501
|
usage = event.data.get("usage", {})
|
|
408
|
-
|
|
409
|
-
#
|
|
502
|
+
|
|
503
|
+
# Extract model information from choice data or event data
|
|
504
|
+
model = choice.get("model", "unknown")
|
|
505
|
+
if model == "unknown":
|
|
506
|
+
# Try to get model from the choice response structure
|
|
507
|
+
if isinstance(choice, dict):
|
|
508
|
+
model = choice.get("model") or choice.get("id", "unknown")
|
|
509
|
+
|
|
510
|
+
# Convert to Langfuse v2 format - let Langfuse handle cost calculation automatically
|
|
410
511
|
langfuse_usage = None
|
|
411
512
|
if usage:
|
|
513
|
+
prompt_tokens = usage.get("prompt_tokens", 0)
|
|
514
|
+
completion_tokens = usage.get("completion_tokens", 0)
|
|
515
|
+
total_tokens = usage.get("total_tokens", 0)
|
|
516
|
+
|
|
412
517
|
langfuse_usage = {
|
|
413
|
-
"input":
|
|
414
|
-
"output":
|
|
415
|
-
"total":
|
|
518
|
+
"input": prompt_tokens,
|
|
519
|
+
"output": completion_tokens,
|
|
520
|
+
"total": total_tokens,
|
|
416
521
|
"unit": "TOKENS"
|
|
417
522
|
}
|
|
418
|
-
|
|
419
|
-
|
|
523
|
+
|
|
524
|
+
print(f"[LANGFUSE] Usage data for automatic cost calculation: {langfuse_usage}")
|
|
525
|
+
|
|
526
|
+
# Include model information in the generation end - Langfuse will calculate costs automatically
|
|
527
|
+
generation.end(
|
|
528
|
+
output=choice,
|
|
529
|
+
usage=langfuse_usage,
|
|
530
|
+
model=model, # Pass model directly for automatic cost calculation
|
|
531
|
+
metadata={
|
|
532
|
+
"model": model,
|
|
533
|
+
"system_fingerprint": choice.get("system_fingerprint"),
|
|
534
|
+
"created": choice.get("created"),
|
|
535
|
+
"response_id": choice.get("id")
|
|
536
|
+
}
|
|
537
|
+
)
|
|
420
538
|
|
|
421
539
|
# Clean up the span reference
|
|
422
540
|
del self.active_spans[span_id]
|
|
423
|
-
print(f"[LANGFUSE] Generation ended")
|
|
541
|
+
print(f"[LANGFUSE] Generation ended with cost tracking")
|
|
424
542
|
else:
|
|
425
543
|
print(f"[LANGFUSE] No generation found for llm_call_end: {span_id}")
|
|
426
544
|
|
|
427
545
|
elif event.type == "tool_call_start":
|
|
428
|
-
# Start a span for tool calls
|
|
429
|
-
|
|
546
|
+
# Start a span for tool calls with detailed input information
|
|
547
|
+
tool_name = event.data.get('tool_name', 'unknown')
|
|
548
|
+
tool_args = event.data.get("args", {})
|
|
549
|
+
|
|
550
|
+
print(f"[LANGFUSE] Starting span for tool call: {tool_name}")
|
|
551
|
+
|
|
552
|
+
# Create comprehensive input data for the tool call
|
|
553
|
+
tool_input = {
|
|
554
|
+
"tool_name": tool_name,
|
|
555
|
+
"arguments": tool_args,
|
|
556
|
+
"call_id": event.data.get("call_id"),
|
|
557
|
+
"timestamp": datetime.now().isoformat()
|
|
558
|
+
}
|
|
559
|
+
|
|
430
560
|
span = self.trace_spans[trace_id].span(
|
|
431
|
-
name=f"tool-{
|
|
432
|
-
input=
|
|
433
|
-
metadata={
|
|
561
|
+
name=f"tool-{tool_name}",
|
|
562
|
+
input=tool_input,
|
|
563
|
+
metadata={
|
|
564
|
+
"tool_name": tool_name,
|
|
565
|
+
"call_id": event.data.get("call_id"),
|
|
566
|
+
"framework": "jaf",
|
|
567
|
+
"event_type": "tool_call"
|
|
568
|
+
}
|
|
434
569
|
)
|
|
435
570
|
span_id = self._get_span_id(event)
|
|
436
571
|
self.active_spans[span_id] = span
|
|
437
|
-
print(f"[LANGFUSE] Created tool span: {
|
|
572
|
+
print(f"[LANGFUSE] Created tool span for {tool_name} with args: {str(tool_args)[:100]}...")
|
|
438
573
|
|
|
439
574
|
elif event.type == "tool_call_end":
|
|
440
575
|
span_id = self._get_span_id(event)
|
|
441
576
|
if span_id in self.active_spans:
|
|
442
|
-
|
|
443
|
-
|
|
577
|
+
tool_name = event.data.get('tool_name', 'unknown')
|
|
578
|
+
tool_result = event.data.get("result")
|
|
579
|
+
|
|
580
|
+
print(f"[LANGFUSE] Ending span for tool call: {tool_name}")
|
|
581
|
+
|
|
582
|
+
# Create comprehensive output data for the tool call
|
|
583
|
+
tool_output = {
|
|
584
|
+
"tool_name": tool_name,
|
|
585
|
+
"result": tool_result,
|
|
586
|
+
"call_id": event.data.get("call_id"),
|
|
587
|
+
"timestamp": datetime.now().isoformat(),
|
|
588
|
+
"status": "completed"
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
# End the span with detailed output
|
|
444
592
|
span = self.active_spans[span_id]
|
|
445
|
-
span.end(
|
|
593
|
+
span.end(
|
|
594
|
+
output=tool_output,
|
|
595
|
+
metadata={
|
|
596
|
+
"tool_name": tool_name,
|
|
597
|
+
"call_id": event.data.get("call_id"),
|
|
598
|
+
"result_length": len(str(tool_result)) if tool_result else 0,
|
|
599
|
+
"framework": "jaf",
|
|
600
|
+
"event_type": "tool_call_end"
|
|
601
|
+
}
|
|
602
|
+
)
|
|
603
|
+
|
|
446
604
|
# Clean up the span reference
|
|
447
605
|
del self.active_spans[span_id]
|
|
448
|
-
print(f"[LANGFUSE] Tool span ended")
|
|
606
|
+
print(f"[LANGFUSE] Tool span ended for {tool_name} with result length: {len(str(tool_result)) if tool_result else 0}")
|
|
449
607
|
else:
|
|
450
608
|
print(f"[LANGFUSE] No tool span found for tool_call_end: {span_id}")
|
|
451
609
|
|
|
@@ -490,7 +648,6 @@ class LangfuseTraceCollector:
|
|
|
490
648
|
return TraceId(event.data['runId'])
|
|
491
649
|
|
|
492
650
|
# Debug: print what's actually in the event data
|
|
493
|
-
print(f"[LANGFUSE] Event data keys: {list(event.data.keys()) if hasattr(event, 'data') and event.data else 'No data'}")
|
|
494
651
|
return None
|
|
495
652
|
|
|
496
653
|
def _get_span_id(self, event: TraceEvent) -> str:
|
|
@@ -502,19 +659,10 @@ class LangfuseTraceCollector:
|
|
|
502
659
|
tool_name = event.data.get('tool_name') or event.data.get('toolName', 'unknown')
|
|
503
660
|
return f"tool-{tool_name}-{trace_id}"
|
|
504
661
|
elif event.type.startswith('llm_call'):
|
|
505
|
-
# For LLM calls,
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if not model and event.type == 'llm_call_end':
|
|
510
|
-
choice = event.data.get('choice', {})
|
|
511
|
-
if isinstance(choice, dict):
|
|
512
|
-
model = choice.get('model')
|
|
513
|
-
|
|
514
|
-
# Handle case where model might be empty or None
|
|
515
|
-
if not model or model == '':
|
|
516
|
-
model = 'unknown'
|
|
517
|
-
return f"llm-{model}-{trace_id}"
|
|
662
|
+
# For LLM calls, use a simpler consistent ID that matches between start and end
|
|
663
|
+
# Get run_id for more consistent matching
|
|
664
|
+
run_id = event.data.get('run_id') or event.data.get('runId', trace_id)
|
|
665
|
+
return f"llm-{run_id}"
|
|
518
666
|
else:
|
|
519
667
|
return f"{event.type}-{trace_id}"
|
|
520
668
|
|
jaf/core/types.py
CHANGED
|
@@ -379,6 +379,8 @@ class RunStartEventData:
|
|
|
379
379
|
run_id: RunId
|
|
380
380
|
trace_id: TraceId
|
|
381
381
|
session_id: Optional[str] = None
|
|
382
|
+
context: Optional[Any] = None
|
|
383
|
+
messages: Optional[List[Message]] = None
|
|
382
384
|
|
|
383
385
|
@dataclass(frozen=True)
|
|
384
386
|
class RunStartEvent:
|
|
@@ -392,6 +394,8 @@ class LLMCallStartEventData:
|
|
|
392
394
|
model: str
|
|
393
395
|
trace_id: TraceId
|
|
394
396
|
run_id: RunId
|
|
397
|
+
context: Optional[Any] = None
|
|
398
|
+
messages: Optional[List[Message]] = None
|
|
395
399
|
|
|
396
400
|
@dataclass(frozen=True)
|
|
397
401
|
class LLMCallStartEvent:
|
|
@@ -42,7 +42,7 @@ 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=v6yXp-zJF9Cj_9qlaW1rmCz7_N3YuDU02xxlzl-howM,27122
|
|
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
|
|
@@ -50,8 +50,8 @@ jaf/core/proxy_helpers.py,sha256=i7a5fAX9rLmO4FMBX51-yRkTFwfWedzQNgnLmeLUd_A,437
|
|
|
50
50
|
jaf/core/streaming.py,sha256=c5o9iqpjoYV2LrUpG6qLWCYrWcP-DCcZsvMbyqKunp8,16089
|
|
51
51
|
jaf/core/tool_results.py,sha256=-bTOqOX02lMyslp5Z4Dmuhx0cLd5o7kgR88qK2HO_sw,11323
|
|
52
52
|
jaf/core/tools.py,sha256=SbJRRr4y_xxNYNTulZg6OiyNaHBlo_qXWYY510jxQEs,16489
|
|
53
|
-
jaf/core/tracing.py,sha256=
|
|
54
|
-
jaf/core/types.py,sha256=
|
|
53
|
+
jaf/core/tracing.py,sha256=3ByTbpYMWHJku-NEasK5ncyMOdv7vHwmG6ybJP19pxE,31659
|
|
54
|
+
jaf/core/types.py,sha256=9UXrPkepw7opgv1VGbPAC1Zx80RP4-ouRxtb6kVTA-A,17063
|
|
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
|
|
@@ -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.3.1.dist-info/licenses/LICENSE,sha256=LXUQBJxdyr-7C4bk9cQBwvsF_xwA-UVstDTKabpcjlI,1063
|
|
83
|
+
jaf_py-2.3.1.dist-info/METADATA,sha256=0o3VxXPHt1FO6lHDttZyFGNDk-WPH5nxaX9zXoAvcLg,27613
|
|
84
|
+
jaf_py-2.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
85
|
+
jaf_py-2.3.1.dist-info/entry_points.txt,sha256=OtIJeNJpb24kgGrqRx9szGgDx1vL9ayq8uHErmu7U5w,41
|
|
86
|
+
jaf_py-2.3.1.dist-info/top_level.txt,sha256=Xu1RZbGaM4_yQX7bpalo881hg7N_dybaOW282F15ruE,4
|
|
87
|
+
jaf_py-2.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|