You can now upload PDFs, CSVs, or images, and the agent will process them accordingly.

#2
.gitignore DELETED
@@ -1,3 +0,0 @@
1
- assets/favicon.ico
2
- favicon.ico
3
- **/favicon.ico
 
 
 
 
README.md CHANGED
@@ -127,19 +127,19 @@ Open http://localhost:7860 in your browser.
127
 
128
  ```
129
  ┌─────────────────────────────────────────────────────────────┐
130
- │ Gradio Web Interface
131
  └─────────────────────────────────────────────────────────────┘
132
 
133
 
134
  ┌─────────────────────────────────────────────────────────────┐
135
- │ Strands Agent
136
  │ ┌─────────────────────────────────────────────────────┐ │
137
- │ │ System Prompt │ │
138
- │ │ "You are a Fraud Model Explainability Assistant..."│ │
139
  │ └─────────────────────────────────────────────────────┘ │
140
-
141
  │ ┌─────────────────────────────────────────────────────┐ │
142
- │ │ Tools │ │
143
  │ │ • get_application_summary │ │
144
  │ │ • explain_fraud_score │ │
145
  │ │ • compare_to_population │ │
@@ -151,7 +151,7 @@ Open http://localhost:7860 in your browser.
151
 
152
 
153
  ┌─────────────────────────────────────────────────────────────┐
154
- │ LLM (GPT-4o / Bedrock)
155
  └─────────────────────────────────────────────────────────────┘
156
  ```
157
 
 
127
 
128
  ```
129
  ┌─────────────────────────────────────────────────────────────┐
130
+ │ Gradio Web Interface
131
  └─────────────────────────────────────────────────────────────┘
132
 
133
 
134
  ┌─────────────────────────────────────────────────────────────┐
135
+ │ Strands Agent
136
  │ ┌─────────────────────────────────────────────────────┐ │
137
+ │ │ System Prompt │ │
138
+ │ │ "You are a Fraud Model Explainability Assistant..." │ │
139
  │ └─────────────────────────────────────────────────────┘ │
140
+
141
  │ ┌─────────────────────────────────────────────────────┐ │
142
+ │ │ Tools │ │
143
  │ │ • get_application_summary │ │
144
  │ │ • explain_fraud_score │ │
145
  │ │ • compare_to_population │ │
 
151
 
152
 
153
  ┌─────────────────────────────────────────────────────────────┐
154
+ │ LLM (GPT-4o / Bedrock)
155
  └─────────────────────────────────────────────────────────────┘
156
  ```
157
 
app.py CHANGED
@@ -55,14 +55,7 @@ from fastapi.middleware.cors import CORSMiddleware
55
  from pydantic import BaseModel
56
 
57
  from strands import Agent
58
- from strands.agent.conversation_manager import SlidingWindowConversationManager
59
  from strands.models.openai import OpenAIModel
60
- from strands.models.openai import OpenAIModel
61
- from strands.handlers.callback_handler import PrintingCallbackHandler
62
-
63
- # Telemetry
64
- from telemetry import setup_telemetry
65
- setup_telemetry()
66
 
67
  # Import confluence-ingestor
68
  from confluence_ingestor import ConfluenceRAG
@@ -87,13 +80,9 @@ from utils import (
87
  # LOGGING CONFIGURATION
88
  # =============================================================================
89
 
90
- # Configure the root strands logger
91
- logging.getLogger("strands").setLevel(logging.DEBUG)
92
-
93
- # Add a handler to see the logs
94
  logging.basicConfig(
95
  level=logging.INFO,
96
- format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
97
  handlers=[
98
  logging.FileHandler('fraud_assistant_confluence.log'),
99
  logging.StreamHandler()
@@ -401,40 +390,14 @@ def create_enhanced_agent():
401
  model_id="gpt-4o",
402
  params={"temperature": 0.1, "max_tokens": 2048},
403
  )
404
- # Create a conversation manager with custom window size
405
- conversation_manager = SlidingWindowConversationManager(
406
- window_size=20, # Maximum number of messages to keep
407
- should_truncate_results=True, # Enable truncating the tool result when a message is too large for the model's context window
408
- )
409
-
410
- # The default callback handler prints text and shows tool usage
411
- _cached_agent = Agent(
412
- model=model,
413
- system_prompt=system_prompt,
414
- tools=tools,
415
- conversation_manager=conversation_manager,
416
- callback_handler=PrintingCallbackHandler()
417
- )
418
  else:
419
- # Create a conversation manager with custom window size
420
- conversation_manager = SlidingWindowConversationManager(
421
- window_size=20, # Maximum number of messages to keep
422
- should_truncate_results=True, # Enable truncating the tool result when a message is too large for the model's context window
423
- )
424
-
425
- # The default callback handler prints text and shows tool usage
426
- _cached_agent = Agent(
427
- system_prompt=system_prompt,
428
- tools=tools,
429
- conversation_manager=conversation_manager,
430
- callback_handler=PrintingCallbackHandler()
431
- )
432
-
433
  return _cached_agent
434
 
435
 
436
-
437
- def query_agent(question: str, files: Optional[List[FilePayload]] = None, return_full_result: bool = False):
438
  """Process question with the enhanced agent, optionally including files."""
439
  try:
440
  logger.info(f"Processing query: {question}")
@@ -443,33 +406,19 @@ def query_agent(question: str, files: Optional[List[FilePayload]] = None, return
443
 
444
  agent = create_enhanced_agent()
445
 
446
- # Base text content
447
- combined_text = question
448
-
449
- # List to hold image blocks
450
- image_blocks = []
451
-
452
  if files:
 
453
  try:
454
- # Import necessary types and libraries inside logic to avoid top-level failures if missing
455
- import io
456
- import csv
457
- import pypdf
458
- from strands.types.content import ImageContent
459
- from strands.types.media import ImageSource
460
 
461
- # Try import python-docx
462
- try:
463
- import docx
464
- except ImportError:
465
- docx = None
466
- logger.warning("python-docx not installed. DOCX support disabled.")
467
-
468
  image_formats = {'png', 'jpeg', 'gif', 'webp', 'jpg'}
469
 
470
  for file_obj in files:
471
  try:
472
- # Decode base64
473
  base64_data = file_obj.data
474
  if "," in base64_data:
475
  base64_data = base64_data.split(",")[1]
@@ -478,104 +427,66 @@ def query_agent(question: str, files: Optional[List[FilePayload]] = None, return
478
  fmt = file_obj.format.lower()
479
 
480
  if fmt in image_formats:
481
- # Handle Image - Keep as rich content
482
  image_block = ImageContent(
483
  format=fmt if fmt != 'jpg' else 'jpeg', # Normalize jpg
484
  source=ImageSource(bytes=file_bytes)
485
  )
486
- image_blocks.append({"image": image_block})
487
  else:
488
- # Handle Document - Extract text and append to question
489
- extracted_text = ""
490
-
491
- if fmt == 'pdf':
492
- try:
493
- pdf_reader = pypdf.PdfReader(io.BytesIO(file_bytes))
494
- for i, page in enumerate(pdf_reader.pages):
495
- try:
496
- text = page.extract_text()
497
- if text:
498
- extracted_text += text + "\n"
499
- except Exception as page_err:
500
- logger.warning(f"Failed to extract text from page {i} of {file_obj.name}: {page_err}")
501
- extracted_text += f"[Error extracting page {i+1}]\n"
502
-
503
- if not extracted_text.strip():
504
- extracted_text = "[No text could be extracted from this PDF. It might be an image-only PDF.]"
505
-
506
- except Exception as pdf_err:
507
- logger.error(f"PDF extraction failed for {file_obj.name}: {pdf_err}")
508
- extracted_text = f"[Error extracting PDF text for {file_obj.name}: {str(pdf_err)}]"
509
-
510
- elif fmt == 'docx':
511
- if docx:
512
- try:
513
- doc = docx.Document(io.BytesIO(file_bytes))
514
- full_text = []
515
- for para in doc.paragraphs:
516
- full_text.append(para.text)
517
- extracted_text = '\n'.join(full_text)
518
-
519
- if not extracted_text.strip():
520
- extracted_text = "[No text found in this DOCX file.]"
521
-
522
- except Exception as docx_err:
523
- logger.error(f"DOCX extraction failed for {file_obj.name}: {docx_err}")
524
- extracted_text = f"[Error extracting DOCX text for {file_obj.name}: {str(docx_err)}]"
525
- else:
526
- extracted_text = "[DOCX support is not available. Please install python-docx.]"
527
-
528
- elif fmt == 'csv':
529
- try:
530
- # Decode bytes to string
531
- csv_text = file_bytes.decode('utf-8', errors='replace')
532
- csv_file = io.StringIO(csv_text)
533
- csv_reader = csv.reader(csv_file)
534
-
535
- rows = []
536
- for row in csv_reader:
537
- rows.append(','.join(row))
538
-
539
- extracted_text = '\n'.join(rows)
540
-
541
- if not extracted_text.strip():
542
- extracted_text = "[Empty CSV file.]"
543
-
544
- except Exception as csv_err:
545
- logger.error(f"CSV extraction failed for {file_obj.name}: {csv_err}")
546
- extracted_text = f"[Error extracting CSV text for {file_obj.name}: {str(csv_err)}]"
547
-
548
- else:
549
- # Try decoding as plain text (txt, md, html, etc)
550
- try:
551
- extracted_text = file_bytes.decode('utf-8', errors='replace')
552
- except Exception as dec_err:
553
- logger.error(f"Text decoding failed for {file_obj.name}: {dec_err}")
554
- extracted_text = f"[Error decoding text for {file_obj.name}]"
555
-
556
- # Append to combined text
557
- combined_text += f"\n\n--- Content from {file_obj.name} ---\n{extracted_text}\n-----------------------------------\n"
558
 
559
  except Exception as err:
560
  logger.error(f"Failed to process file {file_obj.name}: {err}")
561
 
562
- except ImportError as ie:
563
- logger.error(f"Missing dependency for file processing: {ie}")
564
- return "Error: Server missing dependencies (pypdf, python-docx, or strands types) for file processing."
 
 
 
565
 
566
- # Construct final payload
567
- message_content = [{"text": combined_text}]
568
- # Add any extracted images
569
- message_content.extend(image_blocks)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
 
571
- # Call agent with list payload
572
- result = agent(message_content)
 
 
 
573
 
574
  logger.info("Query completed successfully")
575
-
576
- if return_full_result:
577
- return result
578
-
579
  return str(result)
580
  except Exception as e:
581
  logger.error(f"Query failed: {e}")
@@ -583,7 +494,6 @@ def query_agent(question: str, files: Optional[List[FilePayload]] = None, return
583
  return f"Error: {str(e)}"
584
 
585
 
586
-
587
  # =============================================================================
588
  # FASTAPI APPLICATION
589
  # =============================================================================
@@ -1178,4 +1088,4 @@ if __name__ == "__main__":
1178
  host="0.0.0.0",
1179
  port=7860,
1180
  reload=False
1181
- )
 
55
  from pydantic import BaseModel
56
 
57
  from strands import Agent
 
58
  from strands.models.openai import OpenAIModel
 
 
 
 
 
 
59
 
60
  # Import confluence-ingestor
61
  from confluence_ingestor import ConfluenceRAG
 
80
  # LOGGING CONFIGURATION
81
  # =============================================================================
82
 
 
 
 
 
83
  logging.basicConfig(
84
  level=logging.INFO,
85
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
86
  handlers=[
87
  logging.FileHandler('fraud_assistant_confluence.log'),
88
  logging.StreamHandler()
 
390
  model_id="gpt-4o",
391
  params={"temperature": 0.1, "max_tokens": 2048},
392
  )
393
+ _cached_agent = Agent(model=model, system_prompt=system_prompt, tools=tools)
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  else:
395
+ _cached_agent = Agent(system_prompt=system_prompt, tools=tools)
396
+
 
 
 
 
 
 
 
 
 
 
 
 
397
  return _cached_agent
398
 
399
 
400
+ def query_agent(question: str, files: Optional[List[FilePayload]] = None) -> str:
 
401
  """Process question with the enhanced agent, optionally including files."""
402
  try:
403
  logger.info(f"Processing query: {question}")
 
406
 
407
  agent = create_enhanced_agent()
408
 
 
 
 
 
 
 
409
  if files:
410
+ # Try to use official types if available
411
  try:
412
+ from strands.types.content import ImageContent, DocumentContent
413
+ from strands.types.media import ImageSource, DocumentSource
414
+
415
+ message_content = [{"text": question}]
 
 
416
 
 
 
 
 
 
 
 
417
  image_formats = {'png', 'jpeg', 'gif', 'webp', 'jpg'}
418
 
419
  for file_obj in files:
420
  try:
421
+ # Remove header if present
422
  base64_data = file_obj.data
423
  if "," in base64_data:
424
  base64_data = base64_data.split(",")[1]
 
427
  fmt = file_obj.format.lower()
428
 
429
  if fmt in image_formats:
430
+ # Handle Image
431
  image_block = ImageContent(
432
  format=fmt if fmt != 'jpg' else 'jpeg', # Normalize jpg
433
  source=ImageSource(bytes=file_bytes)
434
  )
435
+ message_content.append({"image": image_block})
436
  else:
437
+ # Handle Document
438
+ doc_block = DocumentContent(
439
+ format=fmt,
440
+ name=file_obj.name,
441
+ source=DocumentSource(bytes=file_bytes)
442
+ )
443
+ message_content.append({"document": doc_block})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
 
445
  except Exception as err:
446
  logger.error(f"Failed to process file {file_obj.name}: {err}")
447
 
448
+ except ImportError:
449
+ # Fallback for older versions or missing imports
450
+ logger.info("Using legacy/dict construction (strands.types not found)")
451
+ message_content = [{"text": question}]
452
+
453
+ image_formats = {'png', 'jpeg', 'gif', 'webp', 'jpg'}
454
 
455
+ for file_obj in files:
456
+ try:
457
+ base64_data = file_obj.data
458
+ if "," in base64_data:
459
+ base64_data = base64_data.split(",")[1]
460
+
461
+ file_bytes = base64.b64decode(base64_data)
462
+ fmt = file_obj.format.lower()
463
+
464
+ if fmt in image_formats:
465
+ message_content.append({
466
+ "image": {
467
+ "format": fmt if fmt != 'jpg' else 'jpeg',
468
+ "source": {"bytes": file_bytes},
469
+ },
470
+ })
471
+ else:
472
+ message_content.append({
473
+ "document": {
474
+ "format": fmt,
475
+ "name": file_obj.name,
476
+ "source": {"bytes": file_bytes},
477
+ },
478
+ })
479
+
480
+ except Exception as err:
481
+ logger.error(f"Failed to process file: {err}")
482
 
483
+ # Call agent with list payload
484
+ result = agent(message_content)
485
+ else:
486
+ # Standard text-only call
487
+ result = agent(question)
488
 
489
  logger.info("Query completed successfully")
 
 
 
 
490
  return str(result)
491
  except Exception as e:
492
  logger.error(f"Query failed: {e}")
 
494
  return f"Error: {str(e)}"
495
 
496
 
 
497
  # =============================================================================
498
  # FASTAPI APPLICATION
499
  # =============================================================================
 
1088
  host="0.0.0.0",
1089
  port=7860,
1090
  reload=False
1091
+ )
evals/README.md DELETED
@@ -1,67 +0,0 @@
1
- # Evaluation Suite
2
-
3
- This directory contains a modular evaluation framework for the Fraud Model Explainability Assistant, built using the Strands Evaluation SDK while maintaining Langfuse + OpenTelemetry integration.
4
-
5
- ## Structure
6
-
7
- ```
8
- evals/
9
- ├── __init__.py # Package initialization
10
- ├── config.py # Shared evaluator configurations
11
- ├── task_functions.py # Agent wrapper functions
12
- ├── langfuse_reporter.py # Langfuse integration
13
- ├── dataset_loader.py # JSON to Case conversion
14
- ├── generate_experiment.py # SDK Experiment Generator
15
- ├── run_helpfulness.py # Helpfulness evaluation
16
- ├── run_trajectory.py # Trajectory evaluation
17
- └── run_full_suite.py # Full evaluation suite
18
- ```
19
-
20
- ## Usage
21
-
22
- ### Run Individual Evaluations
23
-
24
- ```bash
25
- # Helpfulness only
26
- python -m evals.run_helpfulness
27
-
28
- # Trajectory only
29
- python -m evals.run_trajectory
30
- ```
31
-
32
- ### Run Full Suite
33
-
34
- ```bash
35
- # All evaluators (Helpfulness, Faithfulness, Trajectory, Goal Success)
36
- python -m evals.run_full_suite
37
- ```
38
-
39
- ### Generate Synthetic Data
40
-
41
- ```bash
42
- # Generate new test cases using SDK ExperimentGenerator
43
- python -m evals.generate_experiment
44
- ```
45
- ## Features
46
-
47
- - **SDK-Aligned**: Uses `Experiment.run_evaluations()` framework
48
- - **Modular**: Each evaluation type in separate file
49
- - **Langfuse Integration**: Automatic score logging with trace correlation
50
- - **OpenTelemetry**: Full observability stack integration
51
- - **Extensible**: Easy to add new evaluators or metrics
52
-
53
- ## Evaluators
54
-
55
- 1. **Helpfulness** (`OutputEvaluator`): Rates answer quality (accuracy, completeness, clarity)
56
- 2. **Faithfulness** (`OutputEvaluator`): Verifies answer is grounded in retrieved context
57
- 3. **Trajectory** (`TrajectoryEvaluator`): Checks tool usage sequence
58
- 4. **Goal Success** (Heuristic): Matches expected key points in answer
59
-
60
- ## Comparison with `evaluate.py`
61
-
62
- The modular suite provides the same functionality as the monolithic `evaluate.py` but with:
63
- - ✅ Better code organization
64
- - ✅ SDK-standard patterns
65
- - ✅ Easier to extend with new evaluators
66
- - ✅ Built-in reporting (`report.run_display()`, `get_summary()`)
67
- - ✅ Maintains Langfuse + OTel integration
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
evals/__init__.py DELETED
@@ -1,8 +0,0 @@
1
- """
2
- Evaluation suite for the Fraud Model Explainability Assistant.
3
-
4
- This package provides modular evaluation capabilities using the Strands SDK
5
- while maintaining integration with Langfuse and OpenTelemetry.
6
- """
7
-
8
- __version__ = "1.0.0"
 
 
 
 
 
 
 
 
 
evals/config.py DELETED
@@ -1,59 +0,0 @@
1
- """
2
- Shared configuration for evaluators and models.
3
- """
4
- from strands.models.openai import OpenAIModel
5
- from strands_evals.evaluators import OutputEvaluator, TrajectoryEvaluator, FaithfulnessEvaluator
6
-
7
- # Configure OpenAI model for evaluators
8
- eval_model = OpenAIModel(model_id="gpt-4o")
9
-
10
- # Helpfulness Evaluator
11
- helpfulness_evaluator = OutputEvaluator(
12
- rubric="""
13
- Evaluate the response based on:
14
- 1. Accuracy - Is the information factually correct?
15
- 2. Completeness - Does it fully answer the question?
16
- 3. Clarity - Is it easy to understand?
17
-
18
- Score 1.0 if all criteria are met excellently.
19
- Score 0.5 if some criteria are partially met.
20
- Score 0.0 if the response is inadequate or incorrect.
21
- """,
22
- include_inputs=True,
23
- model=eval_model
24
- )
25
-
26
- # Faithfulness Evaluator (Generic for now, to enable data generation)
27
- faithfulness_evaluator = OutputEvaluator(
28
- rubric="""
29
- Evaluate if the response is faithful to the retrieved context.
30
-
31
- Score 1.0 if fully supported by context.
32
- Score 0.5 if partially supported or context unavailable.
33
- Score 0.0 if contains hallucinations or contradicts context.
34
-
35
- Penalize heavily for information NOT in the context.
36
- """,
37
- include_inputs=True,
38
- model=eval_model
39
- )
40
-
41
- # Trajectory Evaluator
42
- trajectory_evaluator = TrajectoryEvaluator(
43
- rubric="""
44
- Evaluate the tool usage trajectory:
45
- 1. Correct tool selection - Were the right tools chosen?
46
- 2. Proper sequence - Logical order (Retrieve -> Analyze -> Explain)?
47
- 3. Efficiency - No unnecessary tools?
48
-
49
- Score 1.0 if optimal tools used correctly.
50
- Score 0.5 if correct tools but suboptimal sequence.
51
- SCORE 0.0 if wrong tools or major inefficiencies.
52
- """,
53
- include_inputs=True,
54
- model=eval_model
55
- )
56
-
57
- # Key Points Evaluator (Custom)
58
- from evals.key_points_evaluator import KeyPointsEvaluator
59
- key_points_evaluator = KeyPointsEvaluator()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
evals/dataset_loader.py DELETED
@@ -1,35 +0,0 @@
1
- """
2
- Dataset loader that converts JSON to SDK Case objects.
3
- """
4
- import json
5
- from typing import List
6
- from strands_evals import Case
7
-
8
-
9
- def load_cases_from_json(filepath: str) -> List[Case]:
10
- """
11
- Load test cases from JSON dataset and convert to SDK Case objects.
12
-
13
- Args:
14
- filepath: Path to JSON dataset file
15
-
16
- Returns:
17
- List of Case objects
18
- """
19
- with open(filepath) as f:
20
- data = json.load(f)
21
-
22
- cases = []
23
- for item in data:
24
- case = Case(
25
- name=item["id"],
26
- input=item["question"],
27
- expected_output=None, # Not used for LLM-based evaluation
28
- metadata={
29
- "expected_key_points": item.get("expected_answer_key_points", []),
30
- "expected_intent": item.get("expected_intent", "")
31
- }
32
- )
33
- cases.append(case)
34
-
35
- return cases
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
evals/generate_experiment.py DELETED
@@ -1,86 +0,0 @@
1
- #!/usr/bin/env python
2
- """
3
- Generate synthetic test cases using Strands ExperimentGenerator.
4
-
5
- This script replaces the custom generate_data.py and uses the SDK
6
- to generate diverse, high-quality test cases for the fraud agent.
7
- """
8
- import os
9
- import json
10
- import asyncio
11
- from dotenv import load_dotenv
12
-
13
- load_dotenv()
14
-
15
- from typing import List, Dict
16
- from strands_evals.generators import ExperimentGenerator
17
- from strands_evals.evaluators import OutputEvaluator
18
- from evals.config import eval_model
19
-
20
- # Context description for the generator
21
- CONTEXT = """
22
- You are generating test cases for a Fraud Model Explainability Assistant for a financial services company.
23
- The assistant uses RAG and tools to explain fraud scores (0-1000), SHAP values, and compliance checks.
24
-
25
- Users are typically:
26
- 1. Fraud Analysts (investigating specific cases)
27
- 2. Data Scientists (monitoring model performance)
28
- 3. Compliance Officers (checking for Fair Lending bias)
29
- 4. Executives (asking for high-level summaries)
30
-
31
- Tools available:
32
- - get_application_summary(app_id): Returns score, risk level.
33
- - explain_fraud_score(app_id): Returns SHAP feature contributions.
34
- - compare_to_population(app_id): Returns stats vs approved/denied.
35
- - check_fair_lending_flags(app_id): Returns bias analysis.
36
- - get_identity_network(app_id): Returns linked applications.
37
- """
38
-
39
- async def generate():
40
- print("🚀 Starting Experiment Generation with SDK...")
41
-
42
- # Initialize generator with str input/output
43
- generator = ExperimentGenerator[str, str](
44
- input_type=str,
45
- output_type=str,
46
- model=eval_model
47
- )
48
-
49
- # Generate experiment
50
- print(" Generating cases (this may take a minute)...")
51
- experiment = await generator.from_context_async(
52
- context=CONTEXT,
53
- num_cases=10, # Generate 10 new cases
54
- evaluator=OutputEvaluator, # Pass class, let generator create rubric
55
- task_description="Explain fraud model decisions and risk factors.",
56
- num_topics=5 # Split across different topics (High Risk, Compliance, etc.)
57
- )
58
-
59
- print(f"✅ Generated {len(experiment.cases)} new test cases.")
60
-
61
- # Convert to our JSON format
62
- new_cases = []
63
- for i, case in enumerate(experiment.cases):
64
- # Metadata might be None
65
- metadata = case.metadata if case.metadata else {}
66
- new_case = {
67
- "id": f"synth_sdk_{i+1}",
68
- "question": case.input,
69
- "expected_intent": metadata.get("topic", "General"),
70
- "expected_answer_key_points": [case.expected_output] if case.expected_output else []
71
- }
72
- new_cases.append(new_case)
73
- print(f" - [{new_case['expected_intent']}] {new_case['question'][:60]}...")
74
-
75
- # Load existing cases to append (optional, or overwrite)
76
- output_path = "evaluation/dataset_sdk.json"
77
-
78
- # Saving to a new file to avoid overwriting the main dataset during this test
79
- with open(output_path, "w") as f:
80
- json.dump(new_cases, f, indent=2)
81
-
82
- print(f"\n💾 Saved {len(new_cases)} cases to {output_path}")
83
- print(" Review the file and merge into evaluation/dataset.json if desired.")
84
-
85
- if __name__ == "__main__":
86
- asyncio.run(generate())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
evals/key_points_evaluator.py DELETED
@@ -1,102 +0,0 @@
1
- """
2
- Custom evaluator for checking if specific key points are present in the response.
3
- """
4
- from typing import Any, List
5
- from strands_evals.evaluators import Evaluator
6
- from strands_evals.types.evaluation import EvaluationData, EvaluationOutput
7
- from strands_evals.types.trace import EvaluationLevel
8
- from typing_extensions import TypeVar
9
-
10
- InputT = TypeVar("InputT")
11
- OutputT = TypeVar("OutputT")
12
-
13
- class KeyPointsEvaluator(Evaluator[InputT, OutputT]):
14
- """Evaluates output by checking for presence of expected key points (keywords/phrases)."""
15
-
16
- evaluation_level = EvaluationLevel.TRACE_LEVEL
17
-
18
- def __init__(self, version: str = "v1"):
19
- super().__init__()
20
- self.version = version
21
-
22
- def evaluate(self, evaluation_case: EvaluationData[InputT, OutputT]) -> List[EvaluationOutput]:
23
- """Synchronous evaluation."""
24
- return self._do_evaluation(evaluation_case)
25
-
26
- async def evaluate_async(self, evaluation_case: EvaluationData[InputT, OutputT]) -> List[EvaluationOutput]:
27
- """Asynchronous evaluation."""
28
- return self._do_evaluation(evaluation_case)
29
-
30
- def _do_evaluation(self, evaluation_case: EvaluationData[InputT, OutputT]) -> List[EvaluationOutput]:
31
- """
32
- Check if expected key points are present in the actual output.
33
- Expects 'expected_key_points' list in case metadata.
34
- """
35
- # Get actual output
36
- actual_output = str(evaluation_case.actual_output)
37
-
38
- # Get expectations from case metadata (which is attached to evaluation_case)
39
- # Note: The SDK passes the whole Case object or relevant parts.
40
- # However, EvaluationData typically has input/output.
41
- # Metadata is likely accessible if evaluation_case is constructed from a Case.
42
- # But SDK EvaluationData doesn't strictly carry metadata field in all versions.
43
- # We rely on how Experiment constructs it.
44
-
45
- # EXPERIMENTAL: The SDK's Experiment loop constructs EvaluationData.
46
- # If it doesn't pass metadata, we need to inspect the source 'case'.
47
- # But Evaluator.evaluate receives EvaluationData, not Case.
48
- # Wait, Strands SDK 1.22 might have metadata on EvaluationData?
49
- # Let's check the type definition if needed.
50
- # For now, assuming we can access it or we need a workaround.
51
-
52
- # Workaround: For this custom evaluator to work with Experiment,
53
- # the Experiment must pass metadata.
54
-
55
- # Actually, looking at the Experiment source (which we can't see right now but inferred),
56
- # it might be easier to pass expected_output as the key points string?
57
- # Dataset loader sets: expected_key_points in metadata.
58
-
59
- # Let's try to access metadata if it exists on EvaluationData,
60
- # Otherwise fall back to a safe default.
61
-
62
- key_points = []
63
- if hasattr(evaluation_case, 'metadata') and evaluation_case.metadata:
64
- key_points = evaluation_case.metadata.get("expected_key_points", [])
65
-
66
- # Calculate score
67
- if not key_points:
68
- return [EvaluationOutput(
69
- score=1.0,
70
- test_pass=True,
71
- reason="No key points defined for this case.",
72
- label="N/A"
73
- )]
74
-
75
- hits = 0
76
- misses = []
77
-
78
- for point in key_points:
79
- point_lower = point.lower()
80
- output_lower = actual_output.lower()
81
-
82
- if point_lower in output_lower:
83
- hits += 1
84
- # partial match check (heuristic from run_full_suite)
85
- elif any(word in output_lower for word in point_lower.split() if len(word) > 4):
86
- hits += 0.5
87
- misses.append(f"{point} (Partial)")
88
- else:
89
- misses.append(point)
90
-
91
- score = min(1.0, hits / len(key_points))
92
-
93
- reason = f"Matched {hits}/{len(key_points)} key points."
94
- if misses:
95
- reason += f" Missed: {', '.join(misses[:3])}..."
96
-
97
- return [EvaluationOutput(
98
- score=score,
99
- test_pass=score >= 0.7, # 70% threshold
100
- reason=reason,
101
- label=f"{int(score*100)}%"
102
- )]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
evals/langfuse_reporter.py DELETED
@@ -1,96 +0,0 @@
1
- """
2
- Langfuse integration for logging evaluation results.
3
- """
4
- import os
5
- import base64
6
- import httpx
7
- from typing import List, Dict
8
- from opentelemetry import trace
9
-
10
-
11
- class LangfuseClient:
12
- """Client for logging evaluation scores to Langfuse."""
13
-
14
- def __init__(self):
15
- self.secret_key = os.environ.get("LANGFUSE_SECRET_KEY")
16
- self.public_key = os.environ.get("LANGFUSE_PUBLIC_KEY")
17
- self.base_url = os.environ.get("LANGFUSE_BASE_URL", "https://cloud.langfuse.com").rstrip("/")
18
-
19
- if not (self.secret_key and self.public_key):
20
- print("⚠ Langfuse credentials missing. Scoring disabled.")
21
- self.enabled = False
22
- return
23
-
24
- self.enabled = True
25
- auth_str = f"{self.public_key}:{self.secret_key}"
26
- auth_bytes = auth_str.encode("ascii")
27
- base64_auth = base64.b64encode(auth_bytes).decode("ascii")
28
- self.headers = {
29
- "Authorization": f"Basic {base64_auth}",
30
- "Content-Type": "application/json"
31
- }
32
-
33
- def score_trace(self, trace_id: str, name: str, value: float, comment: str = None):
34
- """Log a single score to Langfuse."""
35
- if not self.enabled:
36
- return
37
-
38
- url = f"{self.base_url}/api/public/scores"
39
- payload = {
40
- "traceId": trace_id,
41
- "name": name,
42
- "value": value,
43
- "comment": comment
44
- }
45
-
46
- try:
47
- resp = httpx.post(url, json=payload, headers=self.headers, timeout=10.0)
48
- if resp.status_code not in (200, 201):
49
- print(f" ⚠ Failed to log score {name}: {resp.status_code} - {resp.text}")
50
- except Exception as e:
51
- print(f" ⚠ Error logging score: {e}")
52
-
53
-
54
- class LangfuseReporter:
55
- """Reporter that logs SDK experiment results to Langfuse."""
56
-
57
- def __init__(self, langfuse_client: LangfuseClient):
58
- self.lf_client = langfuse_client
59
- self.tracer = trace.get_tracer("evaluation_reporter")
60
-
61
- def log_experiment_results(self, reports, case_names: List[str], evaluator_names: List[str] = None) -> Dict[str, str]:
62
- """
63
- Log SDK experiment results to Langfuse with OpenTelemetry trace correlation.
64
-
65
- Args:
66
- reports: List of evaluation reports from Experiment.run_evaluations()
67
- case_names: List of case names to create trace IDs for
68
- evaluator_names: Optional list of evaluator names. If not provided, generic names will be used.
69
-
70
- Returns:
71
- Dict mapping case names to trace IDs
72
- """
73
- trace_ids = {}
74
-
75
- for i, case_name in enumerate(case_names):
76
- # Create OTel span for this case
77
- with self.tracer.start_as_current_span(f"Eval: {case_name}") as span:
78
- # Get Trace ID
79
- trace_id_int = span.get_span_context().trace_id
80
- trace_id_hex = "{:032x}".format(trace_id_int)
81
- trace_ids[case_name] = trace_id_hex
82
-
83
- # Log all evaluator scores for this case
84
- for j, report in enumerate(reports):
85
- if i < len(report.scores):
86
- eval_name = evaluator_names[j] if evaluator_names and j < len(evaluator_names) else f"Evaluator_{j}"
87
- eval_name = eval_name.replace("Evaluator", "")
88
-
89
- self.lf_client.score_trace(
90
- trace_id=trace_id_hex,
91
- name=eval_name,
92
- value=report.scores[i],
93
- comment=report.reasons[i] if i < len(report.reasons) else None
94
- )
95
-
96
- return trace_ids
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
evals/run_full_suite.py DELETED
@@ -1,85 +0,0 @@
1
- #!/usr/bin/env python
2
- """
3
- Run the full evaluation suite on the fraud agent.
4
-
5
- This runs all evaluators (Helpfulness, Faithfulness, Trajectory, Goal Success)
6
- and logs results to Langfuse with OpenTelemetry trace correlation.
7
- """
8
- import json
9
- from strands_evals import Experiment
10
- from strands_evals.types.evaluation import EvaluationData
11
- from evals.config import helpfulness_evaluator, faithfulness_evaluator, trajectory_evaluator, key_points_evaluator
12
- from evals.task_functions import get_fraud_explanation_with_trace
13
- from evals.dataset_loader import load_cases_from_json
14
- from evals.langfuse_reporter import LangfuseClient, LangfuseReporter
15
- from evals.utils import get_report_summary
16
-
17
-
18
- def main():
19
- print("🚀 Starting Full Evaluation Suite\n")
20
- print("="*60)
21
-
22
- # Load test cases
23
- cases = load_cases_from_json("evaluation/dataset.json")
24
- print(f"📋 Loaded {len(cases)} test cases\n")
25
-
26
- # Initialize Langfuse
27
- lf_client = LangfuseClient()
28
- reporter = LangfuseReporter(lf_client)
29
-
30
- # Run evaluations with all evaluators
31
- print("Running evaluations with SDK Experiment framework...")
32
- print("Evaluators: Helpfulness, Faithfulness, Trajectory\n")
33
-
34
- experiment = Experiment(
35
- cases=cases,
36
- evaluators=[
37
- helpfulness_evaluator,
38
- faithfulness_evaluator,
39
- trajectory_evaluator,
40
- key_points_evaluator
41
- ]
42
- )
43
-
44
- reports = experiment.run_evaluations(get_fraud_explanation_with_trace)
45
-
46
- # Display results for each evaluator
47
- print("\n" + "="*60)
48
- print("EVALUATION RESULTS")
49
- print("="*60 + "\n")
50
-
51
- for i, report in enumerate(reports):
52
- evaluator_name = type(experiment.evaluators[i]).__name__
53
- print(f"\n### {evaluator_name} ###")
54
- report.display()
55
-
56
- summary = get_report_summary(report)
57
- print(f"\n📊 {evaluator_name} Summary:")
58
- print(f" Pass Rate: {summary['pass_rate']:.1%}")
59
- print(f" Average Score: {summary['average_score']:.2f}")
60
- print()
61
-
62
- # Log to Langfuse
63
- print("\n" + "="*60)
64
- print("📤 Logging to Langfuse...")
65
- case_names = [case.name for case in cases]
66
- evaluator_names = [type(e).__name__ for e in experiment.evaluators]
67
- trace_ids = reporter.log_experiment_results(reports, case_names, evaluator_names)
68
- print(f" ✅ Logged {len(trace_ids)} traces with {len(reports)} metrics each")
69
-
70
- # Save experiment
71
- experiment.to_file("experiment_files/full_suite_eval")
72
- print("\n💾 Experiment saved to ./experiment_files/full_suite_eval.json")
73
-
74
- # Final summary
75
- print("\n" + "="*60)
76
- print("✅ EVALUATION COMPLETE")
77
- print("="*60)
78
- print(f"\nTotal Cases: {len(cases)}")
79
- print(f"Evaluators: {len(reports)}")
80
- print(f"Langfuse Traces: {len(trace_ids)}")
81
- print(f"\nView results in Langfuse dashboard")
82
-
83
-
84
- if __name__ == "__main__":
85
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
evals/run_helpfulness.py DELETED
@@ -1,58 +0,0 @@
1
- #!/usr/bin/env python
2
- """
3
- Run helpfulness evaluation on the fraud agent.
4
-
5
- This is equivalent to basic_eval.py from the SDK quickstart,
6
- but customized for the fraud explainability use case.
7
- """
8
- from strands_evals import Experiment
9
- from evals.config import helpfulness_evaluator
10
- from evals.task_functions import get_fraud_explanation
11
- from evals.dataset_loader import load_cases_from_json
12
- from evals.langfuse_reporter import LangfuseClient, LangfuseReporter
13
- from evals.utils import get_report_summary
14
-
15
-
16
- def main():
17
- print("=== Helpfulness Evaluation ===\n")
18
-
19
- # Load test cases
20
- cases = load_cases_from_json("evaluation/dataset.json")
21
- print(f"Loaded {len(cases)} test cases\n")
22
-
23
- # Create experiment
24
- experiment = Experiment(
25
- cases=cases,
26
- evaluators=[helpfulness_evaluator]
27
- )
28
-
29
- # Run evaluations
30
- print("Running evaluations...")
31
- reports = experiment.run_evaluations(get_fraud_explanation)
32
-
33
- # Display SDK results
34
- print("\n" + "="*60)
35
- reports[0].display()
36
-
37
- # Get summary
38
- summary = get_report_summary(reports[0])
39
- print(f"\n📊 Summary:")
40
- print(f" Pass Rate: {summary['pass_rate']:.1%}")
41
- print(f" Average Score: {summary['average_score']:.2f}")
42
-
43
- # Log to Langfuse
44
- print("\n📤 Logging to Langfuse...")
45
- lf_client = LangfuseClient()
46
- reporter = LangfuseReporter(lf_client)
47
- case_names = [case.name for case in cases]
48
- evaluator_names = [type(e).__name__ for e in experiment.evaluators]
49
- trace_ids = reporter.log_experiment_results(reports, case_names, evaluator_names)
50
- print(f" Logged {len(trace_ids)} traces to Langfuse")
51
-
52
- # Save experiment
53
- experiment.to_file("experiment_files/helpfulness_eval")
54
- print("\n💾 Experiment saved to ./experiment_files/helpfulness_eval.json")
55
-
56
-
57
- if __name__ == "__main__":
58
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
evals/run_trajectory.py DELETED
@@ -1,57 +0,0 @@
1
- #!/usr/bin/env python
2
- """
3
- Run trajectory evaluation on the fraud agent.
4
-
5
- This evaluates whether the agent uses the correct tools in the right sequence.
6
- """
7
- from strands_evals import Experiment
8
- from evals.config import trajectory_evaluator
9
- from evals.task_functions import get_fraud_explanation_with_trace
10
- from evals.dataset_loader import load_cases_from_json
11
- from evals.langfuse_reporter import LangfuseClient, LangfuseReporter
12
- from evals.utils import get_report_summary
13
-
14
-
15
- def main():
16
- print("=== Trajectory Evaluation ===\n")
17
-
18
- # Load test cases
19
- cases = load_cases_from_json("evaluation/dataset.json")
20
- print(f"Loaded {len(cases)} test cases\n")
21
-
22
- # Create experiment
23
- experiment = Experiment(
24
- cases=cases,
25
- evaluators=[trajectory_evaluator]
26
- )
27
-
28
- # Run evaluations
29
- print("Running evaluations...")
30
- reports = experiment.run_evaluations(get_fraud_explanation_with_trace)
31
-
32
- # Display SDK results
33
- print("\n" + "="*60)
34
- reports[0].display()
35
-
36
- # Get summary
37
- summary = get_report_summary(reports[0])
38
- print(f"\n📊 Summary:")
39
- print(f" Pass Rate: {summary['pass_rate']:.1%}")
40
- print(f" Average Score: {summary['average_score']:.2f}")
41
-
42
- # Log to Langfuse
43
- print("\n📤 Logging to Langfuse...")
44
- lf_client = LangfuseClient()
45
- reporter = LangfuseReporter(lf_client)
46
- case_names = [case.name for case in cases]
47
- evaluator_names = [type(e).__name__ for e in experiment.evaluators]
48
- trace_ids = reporter.log_experiment_results(reports, case_names, evaluator_names)
49
- print(f" Logged {len(trace_ids)} traces to Langfuse")
50
-
51
- # Save experiment
52
- experiment.to_file("experiment_files/trajectory_eval")
53
- print("\n💾 Experiment saved to ./experiment_files/trajectory_eval.json")
54
-
55
-
56
- if __name__ == "__main__":
57
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
evals/task_functions.py DELETED
@@ -1,64 +0,0 @@
1
- """
2
- Task functions that wrap the fraud agent for evaluation.
3
- """
4
- from typing import List, Tuple
5
- from strands_evals import Case
6
- from app import query_agent
7
-
8
-
9
- def extract_context_and_tools(agent_result) -> Tuple[str, List[str]]:
10
- """Extracts retrieved text and tool names from AgentResult."""
11
- context = []
12
- tool_calls = []
13
-
14
- if not hasattr(agent_result, 'trace') or not agent_result.trace:
15
- return "", []
16
-
17
- for span in agent_result.trace.spans:
18
- # Check for tool execution spans
19
- if hasattr(span, 'span_type') and str(span.span_type) == 'tool_execution':
20
- # Tool Name
21
- tool_name = span.tool_call.name
22
- tool_calls.append(tool_name)
23
-
24
- # Context from Search/Load Tools
25
- if 'confluence' in tool_name or 'get_application_summary' in tool_name or 'compare' in tool_name:
26
- context.append(f"Source ({tool_name}): {span.tool_result.content}")
27
-
28
- return "\n\n".join(context), tool_calls
29
-
30
-
31
- def get_fraud_explanation(case: Case) -> str:
32
- """
33
- Task function for basic output evaluation.
34
-
35
- Args:
36
- case: Test case with input question
37
-
38
- Returns:
39
- Agent's response as string
40
- """
41
- result = query_agent(case.input, return_full_result=False)
42
- return str(result)
43
-
44
-
45
- def get_fraud_explanation_with_trace(case: Case) -> dict:
46
- """
47
- Task function for trajectory and faithfulness evaluation.
48
-
49
- Args:
50
- case: Test case with input question
51
-
52
- Returns:
53
- Dict with output, trajectory, and context
54
- """
55
- result = query_agent(case.input, return_full_result=True)
56
-
57
- # Extract context and tools
58
- context, tools = extract_context_and_tools(result)
59
-
60
- return {
61
- "output": str(result),
62
- "trajectory": tools,
63
- "context": context
64
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
evals/utils.py DELETED
@@ -1,15 +0,0 @@
1
- """
2
- Utility functions for evaluations.
3
- """
4
-
5
- def get_report_summary(report):
6
- """
7
- Calculate summary metrics from an EvaluationReport.
8
-
9
- Returns:
10
- dict: A dictionary containing 'pass_rate' and 'average_score'.
11
- """
12
- return {
13
- "pass_rate": sum(report.test_passes) / len(report.test_passes) if report.test_passes else 0,
14
- "average_score": report.overall_score
15
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
evaluation/dataset.json DELETED
@@ -1,314 +0,0 @@
1
- [
2
- {
3
- "id": "case_1",
4
- "question": "Why was application APP-78432 flagged as high risk?",
5
- "expected_intent": "Analyze why application APP-78432 was flagged",
6
- "expected_answer_key_points": [
7
- "Fraud Score: 449",
8
- "Risk Level: MEDIUM",
9
- "Model: XGBoost Fraud Ensemble v3.2",
10
- "Decision: APPROVED"
11
- ]
12
- },
13
- {
14
- "id": "case_2",
15
- "question": "Check fair lending compliance for APP-55555",
16
- "expected_intent": "Check fair lending compliance",
17
- "expected_answer_key_points": [
18
- "No geographic, name-based, or age-related discrimination",
19
- "Adverse Impact Ratio: 0.94",
20
- "Status: COMPLIANT"
21
- ]
22
- },
23
- {
24
- "id": "case_3",
25
- "question": "Explain the fraud score for APP-12345 and compare it to approved applications",
26
- "expected_intent": "Explain fraud score and compare to population",
27
- "expected_answer_key_points": [
28
- "Fraud Score: 850",
29
- "Risk Level: HIGH",
30
- "Suspicious Pattern: High velocity of applications",
31
- "Identity verification usage"
32
- ]
33
- },
34
- {
35
- "id": "synth_4_synth_high_risk_1",
36
- "question": "Why is this account flagged as high risk for synthetic identity fraud?",
37
- "expected_intent": "Understand the reasons behind the high-risk classification.",
38
- "expected_answer_key_points": [
39
- "Unusual account activity patterns",
40
- "Mismatch between identity attributes",
41
- "Use of known fraudulent identifiers"
42
- ]
43
- },
44
- {
45
- "id": "synth_5_synth_high_risk_2",
46
- "question": "What specific behaviors in the transaction history indicate synthetic identity fraud?",
47
- "expected_intent": "Identify behaviors that suggest synthetic identity fraud.",
48
- "expected_answer_key_points": [
49
- "Multiple transactions with inconsistent locations",
50
- "Rapid account activity after dormancy",
51
- "Attempts to access high-value services"
52
- ]
53
- },
54
- {
55
- "id": "synth_6_synth_high_risk_3",
56
- "question": "How does the model differentiate between synthetic identities and legitimate customers?",
57
- "expected_intent": "Understand the model's methodology for distinguishing between synthetic and legitimate identities.",
58
- "expected_answer_key_points": [
59
- "Advanced pattern recognition algorithms",
60
- "Comparison against known legitimate customer profiles",
61
- "Analysis of identity verification documentation"
62
- ]
63
- },
64
- {
65
- "id": "synth_7_synth_high_risk_4",
66
- "question": "What are the common data points used by the model to detect synthetic identities?",
67
- "expected_intent": "Identify key data points used by the model for detection.",
68
- "expected_answer_key_points": [
69
- "Social security number validation",
70
- "Cross-referencing with public records",
71
- "Analysis of digital footprint and device information"
72
- ]
73
- },
74
- {
75
- "id": "synth_8_low_risk_fp_1",
76
- "question": "Why was this transaction flagged as fraudulent despite being low risk?",
77
- "expected_intent": "Understand the reasons behind a false positive classification.",
78
- "expected_answer_key_points": [
79
- "Unusual transaction pattern",
80
- "Customer purchase history",
81
- "Changes in location or device used"
82
- ]
83
- },
84
- {
85
- "id": "synth_9_low_risk_fp_2",
86
- "question": "Can you explain why this account's activity is considered suspicious?",
87
- "expected_intent": "Gain insights into the factors influencing the fraud model's decision.",
88
- "expected_answer_key_points": [
89
- "High volume of transactions",
90
- "Transaction value deviations",
91
- "Comparison with typical user behavior"
92
- ]
93
- },
94
- {
95
- "id": "synth_10_low_risk_fp_3",
96
- "question": "What factors led to this low-risk transaction being marked as fraud?",
97
- "expected_intent": "Identify specific elements that triggered the fraud alert.",
98
- "expected_answer_key_points": [
99
- "Mismatch in expected transaction time",
100
- "Discrepancy in user verification",
101
- "Recent account changes or updates"
102
- ]
103
- },
104
- {
105
- "id": "synth_11_low_risk_fp_4",
106
- "question": "Why did our system mistakenly flag this legitimate transaction?",
107
- "expected_intent": "Determine the cause of the system error leading to a false positive.",
108
- "expected_answer_key_points": [
109
- "Error in model threshold setting",
110
- "Anomaly detection misclassification",
111
- "Lack of data on new customer behavior"
112
- ]
113
- },
114
- {
115
- "id": "synth_12_synth_borderline_case_1",
116
- "question": "Why is this transaction flagged as suspicious when the customer has a long history of legitimate purchases?",
117
- "expected_intent": "Understand mixed signals causing a fraud alert.",
118
- "expected_answer_key_points": [
119
- "Analyzing recent transaction behavior",
120
- "Comparing with customer's purchase history",
121
- "Highlighting unusual transaction patterns"
122
- ]
123
- },
124
- {
125
- "id": "synth_13_synth_borderline_case_2",
126
- "question": "Can you explain why this low amount transaction is considered high-risk while the customer has been verified recently?",
127
- "expected_intent": "Clarify factors contributing to risk evaluation.",
128
- "expected_answer_key_points": [
129
- "Risk assessment includes transaction context",
130
- "Verification status vs. transaction attributes",
131
- "Analysis of current vs. past behavior"
132
- ]
133
- },
134
- {
135
- "id": "synth_14_synth_borderline_case_3",
136
- "question": "This merchant is reputable, so why are some of their transactions flagged for potential fraud?",
137
- "expected_intent": "Investigate reasons behind inconsistent fraud signals.",
138
- "expected_answer_key_points": [
139
- "Merchant transaction volume vs. individual transactions",
140
- "Potential changes in merchant activities",
141
- "Flagged patterns specific to current transactions"
142
- ]
143
- },
144
- {
145
- "id": "synth_15_synth_borderline_case_4",
146
- "question": "Why is this account flagged when the user is known for frequent travel and varied spending patterns?",
147
- "expected_intent": "Identify causes of false positives in fraud detection.",
148
- "expected_answer_key_points": [
149
- "Account flagged due to recent activity anomalies",
150
- "Analysis of travel-related spending patterns",
151
- "Consideration of geographic and spending behavior"
152
- ]
153
- },
154
- {
155
- "id": "synth_16_fair_lending_compliance_1",
156
- "question": "Why was the loan application of a minority applicant flagged as high risk by the model?",
157
- "expected_intent": "Understand the factors leading to the high-risk categorization for minority applicants.",
158
- "expected_answer_key_points": [
159
- "Model's risk assessment criteria",
160
- "Applicant's credit history and income",
161
- "Any bias or unfair treatment in the model"
162
- ]
163
- },
164
- {
165
- "id": "synth_17_fair_lending_compliance_2",
166
- "question": "Can you explain if the model's decision impacts applicants from low-income neighborhoods differently?",
167
- "expected_intent": "Determine if there is any disparate impact based on the applicant's neighborhood.",
168
- "expected_answer_key_points": [
169
- "Impact of location on risk scoring",
170
- "Comparison of approval rates by neighborhood",
171
- "Fair lending compliance measures in place"
172
- ]
173
- },
174
- {
175
- "id": "synth_18_fair_lending_compliance_3",
176
- "question": "What measures are in place to ensure that the model's decisions comply with fair lending regulations?",
177
- "expected_intent": "Identify compliance protocols for fair lending regulations within the model.",
178
- "expected_answer_key_points": [
179
- "Regular bias audits",
180
- "Use of fair lending algorithms",
181
- "Continuous monitoring for compliance"
182
- ]
183
- },
184
- {
185
- "id": "synth_19_fair_lending_compliance_4",
186
- "question": "How does the model ensure equal treatment for applicants with similar financial profiles but from different demographic groups?",
187
- "expected_intent": "Verify the model's fairness and equality in decision-making across demographics.",
188
- "expected_answer_key_points": [
189
- "Demographic parity in decision-making",
190
- "Comparative analysis of similar profiles",
191
- "Adjustments made to mitigate bias"
192
- ]
193
- },
194
- {
195
- "id": "synth_20_synth_high_risk_1",
196
- "question": "Why is the model flagging an unusually high number of transactions as high-risk this month?",
197
- "expected_intent": "Understand the reason behind the spike in high-risk transaction flags.",
198
- "expected_answer_key_points": [
199
- "Change in transaction patterns",
200
- "Adjustment in model thresholds",
201
- "New data inputs affecting model behavior"
202
- ]
203
- },
204
- {
205
- "id": "synth_21_synth_low_accuracy_2",
206
- "question": "What could be causing the recent drop in model accuracy for identifying fraudulent transactions?",
207
- "expected_intent": "Identify factors leading to decreased model accuracy.",
208
- "expected_answer_key_points": [
209
- "Data drift or changes in data distribution",
210
- "Outdated model parameters",
211
- "Need for model retraining"
212
- ]
213
- },
214
- {
215
- "id": "synth_22_synth_segment_discrepancy_3",
216
- "question": "Why is the model performing poorly on certain customer segments?",
217
- "expected_intent": "Explain performance discrepancies across customer segments.",
218
- "expected_answer_key_points": [
219
- "Segment-specific behavior not well-captured",
220
- "Imbalanced training data for segments",
221
- "Potential need for segment-specific features"
222
- ]
223
- },
224
- {
225
- "id": "synth_23_synth_feature_importance_4",
226
- "question": "What are the most influential features the model uses to determine fraud risk?",
227
- "expected_intent": "Identify key features driving model predictions.",
228
- "expected_answer_key_points": [
229
- "List of top contributing features",
230
- "Feature importance ranking",
231
- "Impact of each feature on the model's decision"
232
- ]
233
- },
234
- {
235
- "id": "synth_sdk_1",
236
- "question": "Application ID: 1023456",
237
- "expected_intent": "General",
238
- "expected_answer_key_points": [
239
- "Fraud Score: 750\nRisk Level: High\nSHAP Values:\n- Transaction History Impact: +200\n- Credit Score Impact: +150\n- Recent Loan Applications Impact: -50\n- Employment History Impact: +100\nExplanation: The application has a high fraud score primarily due to the transaction history and credit score, which together contribute significantly to the high risk level. The recent loan applications positively influence the score by lowering it slightly, but not enough to offset the other risk factors. Employment history further increases the score. It's essential to focus on improving transaction patterns to lower the fraud score."
240
- ]
241
- },
242
- {
243
- "id": "synth_sdk_2",
244
- "question": "Application ID: 4321XYZ\n- Request from Fraud Analyst: \"Please provide a detailed breakdown of the fraud score for this application. Include key feature contributions and provide an explanation for any features that heavily influence the score.\"\n- Tools and Capabilities Requested:\n - use explain_fraud_score(4321XYZ) to highlight SHAP values.\n - identify high-contributing features impacting fraud score.\n - analyze borderline scores and explain if any apply.",
245
- "expected_intent": "General",
246
- "expected_answer_key_points": [
247
- "Fraud Score: 680 (Moderate Risk)\nKey SHAP Contribution Analysis:\n1. \"Credit History Length\": +45 (Positive impact due to long credit history)\n2. \"Number of Recent Applications\": +100 (Negative impact due to recent application spike)\n3. \"Unusual Geolocation Activity\": +150 (High negative impact due to recent activity in a high-risk zone)\n4. \"Income Verification\": -65 (Positive impact due to verified and stable income)\n\nExplanation:\n- The fraud score is boosted to a higher risk mainly due to the unusual geolocation activity and the number of recent applications, which are significant red flags in fraud detection. While the credit history length and verified income provide a buffer reducing the risk level somewhat, the overall risk remains moderate.\n- Due to the combination and magnitude of these factors, the borderline score reflects an increased risk with possibilities of both legitimate and fraudulent activities present."
248
- ]
249
- },
250
- {
251
- "id": "synth_sdk_3",
252
- "question": "Task: Analyze the performance of the fraud detection model over the past six months.\n\nData Provided: \n- List of application IDs from the last six months\n- Access to tools: compare_to_population, explain_fraud_score\n\nKey Question: What trends can be identified in the distribution of fraud scores and SHAP value importance over this period?",
253
- "expected_intent": "General",
254
- "expected_answer_key_points": [
255
- "Output:\n1. Statistical report indicating any significant trends or shifts in fraud scores over the six-month period using compare_to_population.\n2. Identification of any changes in SHAP feature importance that may indicate shifts in model predictions.\n3. Summary of observations highlighting potential model drift or changes in population characteristics that could impact fraud detection accuracy."
256
- ]
257
- },
258
- {
259
- "id": "synth_sdk_4",
260
- "question": "A data scientist is analyzing the fraud detection model's performance over the past six months for applications in a specific region. They want to understand how the model's scoring distribution has shifted over this period and identify any significant changes in feature importance that might correspond with this shift. Use the tools compare_to_population and explain_fraud_score. The focus is on applications in the Midwest region, with application IDs ranging from 1001 to 1050. The model should track score distribution shifts and highlight any significant changes in SHAP feature importance.",
261
- "expected_intent": "General",
262
- "expected_answer_key_points": [
263
- "1. Provide a trend report showing score distribution changes over the past six months for the specified application range in the Midwest, indicating whether fraud scores have increased, decreased, or remained stable.\n2. Use compare_to_population to determine how these changes compare to national trends, noting any significant deviations.\n3. Highlight significant shifts in SHAP value contributions for key features over the same period, indicating which features have become more or less significant in model decision-making.\n4. Correlate findings with any historical applications data to suggest possible reasons for observed shifts, such as policy changes or external economic factors."
264
- ]
265
- },
266
- {
267
- "id": "synth_sdk_5",
268
- "question": "The Compliance Officer wants to verify if there is any evidence of gender bias in fraud scoring for the application ID: F12345. Use the 'check_fair_lending_flags' to detect bias indicators and 'compare_to_population' to analyze demographic-specific fraud score patterns.",
269
- "expected_intent": "General",
270
- "expected_answer_key_points": [
271
- "The 'check_fair_lending_flags' tool indicates a potential bias against female applicants due to disparate impact ratios below threshold in the fraud scoring process, indicating females are disproportionately denied compared to males. 'compare_to_population' further shows that female applicants have consistently higher average fraud scores compared to male counterparts for similar profiles, suggesting an underlying model bias."
272
- ]
273
- },
274
- {
275
- "id": "synth_sdk_6",
276
- "question": "compliance_officer: \"I need to verify if application 1234 has any fair lending bias issues. Check for any bias flags and provide basic SHAP contributions for this case.\"",
277
- "expected_intent": "General",
278
- "expected_answer_key_points": [
279
- "1. Use check_fair_lending_flags(app_id=1234) to identify if there are any bias issues. The tool will return 'No Bias Detected' or 'Potential Bias Detected'.\n2. If bias is present, explain_fraud_score(app_id=1234) to understand which features contribute most to the score.\n3. Report: 'No Bias Detected. Main factors influencing the fraud score are {feature1, feature2} contributing {value1, value2}.' OR 'Potential Bias Detected. SHAP analysis reveals major contributors: {biased_feature} with {value}.'"
280
- ]
281
- },
282
- {
283
- "id": "synth_sdk_7",
284
- "question": "You are tasked with preparing an executive summary that highlights the current trends in fraud detection for the past quarter for the executive team of the financial services company. Use the following application IDs: ['A12345', 'B67890', 'C13579'].",
285
- "expected_intent": "General",
286
- "expected_answer_key_points": [
287
- "The executive summary should include:\n1. A concise summary of the fraud scores and risk levels for each application using 'get_application_summary' for the IDs 'A12345', 'B67890', 'C13579'.\n2. A high-level analysis showing any evident trends in fraud scores over the quarter period.\n3. A brief mention of major contributing factors to the high fraud scores, utilizing SHAP values (not the specifics, just an overview of the top features contributing).\n4. A concise explanation of the application network status for each of these IDs using 'get_identity_network'.\n\nThe summary should be formatted in a report-friendly manner, prioritizing clarity and brevity for executive review."
288
- ]
289
- },
290
- {
291
- "id": "synth_sdk_8",
292
- "question": "Generate a high-level summary for executives on the fraud score trends and key contributing factors for applications over the last quarter. Include how applications are linked within the identity network for potential collusion. Use the following application IDs: ['A123', 'B456', 'C789'].",
293
- "expected_intent": "General",
294
- "expected_answer_key_points": [
295
- "The analysis for the last quarter indicates a rising trend in fraud scores, with the average score increasing by approximately 15% compared to the previous period. Key contributing factors identified through SHAP analysis include unusually high transaction volumes and discrepancies in reported income. Specific to applications ['A123', 'B456', 'C789'], notable features contributing to high fraud scores were similar across the board, indicating potential systemic fraud patterns. Moreover, the get_identity_network for these applications reveals interconnected relationships, suggesting possible collusion attempts. The network analysis indicates that several other applications are linked through shared contact information and transaction patterns, necessitating further investigation into these connections. Overall, the data suggests an evolving strategy that involves increasing sophistication in application profiles that require continued vigilance and adaptation of fraud detection measures."
296
- ]
297
- },
298
- {
299
- "id": "synth_sdk_9",
300
- "question": "Application ID: 12345; Requested Action: Analyze impact of linked applications on fraud score using get_identity_network.",
301
- "expected_intent": "General",
302
- "expected_answer_key_points": [
303
- "get_identity_network(12345) returns {\"linked_applications\": [\"11111\", \"22222\", \"33333\"]}. explain_fraud_score(12345) returns {\"SHAP_values\": {\"income\": -100, \"credit_history\": 50, \"linked_applications\": 200}}. compare_to_population(12345) shows linked applications influence fraud score to be 20% higher than similar approved applications. Risk level classified as 'Moderate Risk' due to negative impact from linked applications."
304
- ]
305
- },
306
- {
307
- "id": "synth_sdk_10",
308
- "question": "Application ID: 12345\nTask: Investigate the fraud score and impact of linked applications using the get_identity_network tool. Provide an analysis of how these linked applications influence the fraud score and SHAP values for Application ID 12345. Use compare_to_population to understand how these impacts differ from the general population.",
309
- "expected_intent": "General",
310
- "expected_answer_key_points": [
311
- "1. Identity Network Output: Application 12345 is linked with applications 56789, 98765, and 65432.\n2. Fraud Score Analysis: The fraud score for Application 12345 is 850. Linked applications have high fraud scores: Application 56789 (900), Application 98765 (875), and Application 65432 (910).\n3. SHAP Values: Key features that increased the fraud score include 'cross-application suspicious transactions' and 'irregular account activities.'\n4. Population Comparison: Compared to the approved applications' average fraud score of 500, Application 12345 and its linked applications show significantly higher risk, suggesting a potential fraud ring.\n5. Conclusion: The linkage to high-risk applications within the identity network explains the elevated fraud score for Application 12345, highlighting the influence of network associations in detecting fraud patterns."
312
- ]
313
- }
314
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -5,21 +5,24 @@ uvicorn[standard]>=0.24.0
5
  python-multipart>=0.0.6
6
 
7
  # Strands Agents SDK with OpenAI provider support
8
- strands-agents[openai,anthropic]>=1.0.0
9
 
10
  # Gradio for web interface
11
  gradio>=4.0.0
12
 
13
  # HTTP client
14
  httpx>=0.24.0
15
- opentelemetry-exporter-otlp>=1.39.1
16
 
17
  # Environment variable management
18
  python-dotenv>=1.0.0
19
 
20
- # Confluence document integration (RAG)
21
  # ==================================================
22
 
 
 
 
 
23
  # confluence-ingestor[strands,huggingface,chroma]>=0.3.0
24
 
25
  # Vector store
@@ -29,10 +32,6 @@ chromadb>=1.4.0
29
  torch>=2.9.1
30
  sentence-transformers>=5.2.0
31
 
32
- # PDF and DOCX text extractor
33
- pypdf>=6.6.0
34
- python-docx>=1.2.0
35
-
36
  # Development/Production (Optional)
37
  # ==================================
38
 
@@ -40,4 +39,4 @@ python-docx>=1.2.0
40
  apscheduler>=3.10.0
41
 
42
  # Logging and monitoring
43
- python-json-logger>=2.0.0
 
5
  python-multipart>=0.0.6
6
 
7
  # Strands Agents SDK with OpenAI provider support
8
+ strands-agents[openai]>=1.0.0
9
 
10
  # Gradio for web interface
11
  gradio>=4.0.0
12
 
13
  # HTTP client
14
  httpx>=0.24.0
 
15
 
16
  # Environment variable management
17
  python-dotenv>=1.0.0
18
 
19
+ # Confluence Integration (Optional but Recommended)
20
  # ==================================================
21
 
22
+ # Confluence document ingestion and RAG
23
+ # Install with: pip install -r requirements-with-confluence.txt
24
+ # Or: pip install confluence-ingestor[strands,huggingface,chroma]
25
+
26
  # confluence-ingestor[strands,huggingface,chroma]>=0.3.0
27
 
28
  # Vector store
 
32
  torch>=2.9.1
33
  sentence-transformers>=5.2.0
34
 
 
 
 
 
35
  # Development/Production (Optional)
36
  # ==================================
37
 
 
39
  apscheduler>=3.10.0
40
 
41
  # Logging and monitoring
42
+ python-json-logger>=2.0.0
telemetry.py DELETED
@@ -1,62 +0,0 @@
1
-
2
- import os
3
- import base64
4
- from opentelemetry import trace
5
- from opentelemetry.sdk.trace import TracerProvider
6
- from opentelemetry.sdk.trace.export import BatchSpanProcessor
7
- from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
8
- from opentelemetry.sdk.resources import Resource
9
-
10
- def setup_telemetry():
11
- """
12
- Configures OpenTelemetry to export traces to Langfuse.
13
- Dependencies: langfuse, opentelemetry-sdk, opentelemetry-exporter-otlp
14
- """
15
-
16
- # helper to check if vars exist
17
- secret_key = os.environ.get("LANGFUSE_SECRET_KEY")
18
- public_key = os.environ.get("LANGFUSE_PUBLIC_KEY")
19
- base_url = os.environ.get("LANGFUSE_BASE_URL", "https://cloud.langfuse.com")
20
-
21
- if not (secret_key and public_key):
22
- print("⚠ Langfuse credentials not found. Telemetry disabled.")
23
- return
24
-
25
- print(f"Initializing Langfuse Telemetry at {base_url}...")
26
-
27
- # Auth Header for Langfuse (Basic Auth)
28
- auth_str = f"{public_key}:{secret_key}"
29
- auth_bytes = auth_str.encode("ascii")
30
- base64_auth = base64.b64encode(auth_bytes).decode("ascii")
31
-
32
- # Configure OTLP HTTP Exporter
33
- # Langfuse OTLP HTTP endpoint: /api/public/otel/v1/traces
34
-
35
- # Construct endpoint
36
- # Remove trailing slash from base
37
- clean_base_url = base_url.rstrip("/")
38
- # Note: OTLPSpanExporter (HTTP) does NOT automatically append /v1/traces in all versions
39
- # or if we provide a full URL. Best to be explicit for Langfuse.
40
- endpoint = f"{clean_base_url}/api/public/otel/v1/traces"
41
-
42
- exporter = OTLPSpanExporter(
43
- endpoint=endpoint,
44
- headers={"Authorization": f"Basic {base64_auth}"}
45
- )
46
-
47
- # Resource
48
- resource = Resource.create(attributes={
49
- "service.name": "fraud_model_explainability_assistant",
50
- "service.version": "1.0.0"
51
- })
52
-
53
- # Provider
54
- provider = TracerProvider(resource=resource)
55
- processor = BatchSpanProcessor(exporter)
56
- provider.add_span_processor(processor)
57
-
58
- # Set global provider
59
- trace.set_tracer_provider(provider)
60
-
61
- print("✅ Langfuse Telemetry configured.")
62
-