Spaces:
Runtime error
Runtime error
Fraud Assistant Workflow Pattern
#6
by chrisjcc - opened
- .gitignore +0 -3
- app.py +25 -20
- docs/implementation_plan.md +48 -0
- docs/multi-agent-pattern-comparison.md +27 -0
- docs/walkthrough.md +55 -0
- evals/README.md +0 -67
- evals/__init__.py +0 -8
- evals/config.py +0 -59
- evals/dataset_loader.py +0 -35
- evals/generate_experiment.py +0 -86
- evals/key_points_evaluator.py +0 -102
- evals/langfuse_reporter.py +0 -96
- evals/run_full_suite.py +0 -85
- evals/run_helpfulness.py +0 -58
- evals/run_trajectory.py +0 -57
- evals/task_functions.py +0 -64
- evals/utils.py +0 -15
- evaluation/dataset.json +0 -314
- requirements.txt +1 -2
- telemetry.py +0 -62
- workflow.py +261 -0
.gitignore
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
assets/favicon.ico
|
| 2 |
-
favicon.ico
|
| 3 |
-
**/favicon.ico
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -57,19 +57,25 @@ from pydantic import BaseModel
|
|
| 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 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
| 66 |
|
| 67 |
# Import confluence-ingestor
|
| 68 |
-
|
| 69 |
-
from confluence_ingestor
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
# Import your existing fraud tools
|
| 75 |
from utils import (
|
|
@@ -187,6 +193,10 @@ def init_confluence():
|
|
| 187 |
print("\n⚠ App will run WITHOUT Confluence integration.\n")
|
| 188 |
raise ValueError(f"Missing Confluence credentials: {', '.join(missing_vars)}")
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
try:
|
| 191 |
_confluence_rag = ConfluenceRAG.from_env(
|
| 192 |
embedding_provider="huggingface",
|
|
@@ -433,15 +443,15 @@ def create_enhanced_agent():
|
|
| 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}")
|
| 441 |
if files:
|
| 442 |
logger.info(f"Query includes {len(files)} files")
|
| 443 |
|
| 444 |
-
|
|
|
|
| 445 |
|
| 446 |
# Base text content
|
| 447 |
combined_text = question
|
|
@@ -568,14 +578,10 @@ def query_agent(question: str, files: Optional[List[FilePayload]] = None, return
|
|
| 568 |
# Add any extracted images
|
| 569 |
message_content.extend(image_blocks)
|
| 570 |
|
| 571 |
-
# Call
|
| 572 |
-
result =
|
| 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 +589,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 |
# =============================================================================
|
|
@@ -618,7 +623,7 @@ async def index():
|
|
| 618 |
async def ask_question(request: QuestionRequest):
|
| 619 |
"""Process a question and return the answer."""
|
| 620 |
try:
|
| 621 |
-
answer = query_agent(request.question, request.files)
|
| 622 |
return AnswerResponse(
|
| 623 |
answer=answer,
|
| 624 |
metrics=_metrics.get_stats()
|
|
|
|
| 57 |
from strands import Agent
|
| 58 |
from strands.agent.conversation_manager import SlidingWindowConversationManager
|
| 59 |
from strands.models.openai import OpenAIModel
|
|
|
|
| 60 |
from strands.handlers.callback_handler import PrintingCallbackHandler
|
| 61 |
|
| 62 |
+
from strands.handlers.callback_handler import PrintingCallbackHandler
|
| 63 |
+
|
| 64 |
+
# Import Workflow
|
| 65 |
+
from workflow import FraudExplainabilityWorkflow
|
| 66 |
|
| 67 |
# Import confluence-ingestor
|
| 68 |
+
try:
|
| 69 |
+
from confluence_ingestor import ConfluenceRAG
|
| 70 |
+
from confluence_ingestor.adapters.strands import (
|
| 71 |
+
create_confluence_search_tool,
|
| 72 |
+
create_confluence_loader_tool,
|
| 73 |
+
)
|
| 74 |
+
except ImportError:
|
| 75 |
+
ConfluenceRAG = None
|
| 76 |
+
create_confluence_search_tool = None
|
| 77 |
+
create_confluence_loader_tool = None
|
| 78 |
+
print("Warning: confluence-ingestor not installed. RAG features disabled.")
|
| 79 |
|
| 80 |
# Import your existing fraud tools
|
| 81 |
from utils import (
|
|
|
|
| 193 |
print("\n⚠ App will run WITHOUT Confluence integration.\n")
|
| 194 |
raise ValueError(f"Missing Confluence credentials: {', '.join(missing_vars)}")
|
| 195 |
|
| 196 |
+
if ConfluenceRAG is None:
|
| 197 |
+
logger.warning("ConfluenceRAG not available (library missing).")
|
| 198 |
+
return None
|
| 199 |
+
|
| 200 |
try:
|
| 201 |
_confluence_rag = ConfluenceRAG.from_env(
|
| 202 |
embedding_provider="huggingface",
|
|
|
|
| 443 |
return _cached_agent
|
| 444 |
|
| 445 |
|
| 446 |
+
async def query_agent(question: str, files: Optional[List[FilePayload]] = None) -> str:
|
|
|
|
| 447 |
"""Process question with the enhanced agent, optionally including files."""
|
| 448 |
try:
|
| 449 |
logger.info(f"Processing query: {question}")
|
| 450 |
if files:
|
| 451 |
logger.info(f"Query includes {len(files)} files")
|
| 452 |
|
| 453 |
+
# Initialize Workflow
|
| 454 |
+
workflow = FraudExplainabilityWorkflow()
|
| 455 |
|
| 456 |
# Base text content
|
| 457 |
combined_text = question
|
|
|
|
| 578 |
# Add any extracted images
|
| 579 |
message_content.extend(image_blocks)
|
| 580 |
|
| 581 |
+
# Call workflow
|
| 582 |
+
result = await workflow.run(input_text=combined_text)
|
| 583 |
|
| 584 |
logger.info("Query completed successfully")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
return str(result)
|
| 586 |
except Exception as e:
|
| 587 |
logger.error(f"Query failed: {e}")
|
|
|
|
| 589 |
return f"Error: {str(e)}"
|
| 590 |
|
| 591 |
|
|
|
|
| 592 |
# =============================================================================
|
| 593 |
# FASTAPI APPLICATION
|
| 594 |
# =============================================================================
|
|
|
|
| 623 |
async def ask_question(request: QuestionRequest):
|
| 624 |
"""Process a question and return the answer."""
|
| 625 |
try:
|
| 626 |
+
answer = await query_agent(request.question, request.files)
|
| 627 |
return AnswerResponse(
|
| 628 |
answer=answer,
|
| 629 |
metrics=_metrics.get_stats()
|
docs/implementation_plan.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Implementation Plan - Fraud Assistant Workflow Pattern
|
| 2 |
+
|
| 3 |
+
## Goal Description
|
| 4 |
+
Transition the `fraud_model_explainability_assistant` from a monolithic `Agent` loop to a structured "Workflow Pattern". This improves **Determinism** (explicit steps), **Auditability** (logging per step), **Reliability** (error handling per step), and enables **Human-in-the-loop** capabilities in the future.
|
| 5 |
+
|
| 6 |
+
The workflow will explicitly orchestrate:
|
| 7 |
+
1. **Intent Analysis**: Understand what the user is asking.
|
| 8 |
+
2. **Routing**: Decide which specialized sub-routine or tool to use.
|
| 9 |
+
3. **Execution**: Run the tools (e.g., `explain_fraud_score`, `check_fair_lending_flags`).
|
| 10 |
+
4. **Synthesis**: Generate the final response based on tool outputs.
|
| 11 |
+
|
| 12 |
+
## User Review Required
|
| 13 |
+
> [!IMPORTANT]
|
| 14 |
+
> This change refactors the core `query_agent` function in `app.py` to use a new `FraudWorkflow` class instead of the direct `strands.Agent` call. The external API endpoints remain the same.
|
| 15 |
+
|
| 16 |
+
## Proposed Changes
|
| 17 |
+
|
| 18 |
+
### Fraud Model Explainability Assistant
|
| 19 |
+
|
| 20 |
+
#### [NEW] [workflow.py](file:///Users/christiancontrerascampana/Desktop/GitHub/syf/fraud_model_explainability_assistant/workflow.py)
|
| 21 |
+
- **`FraudWorkflowState`**: A TypedDict/Pydantic model to hold the conversation state (messages, current context, tool outputs).
|
| 22 |
+
- **`FraudWorkflow` Class**:
|
| 23 |
+
- `__init__`: Initialize models and tools.
|
| 24 |
+
- `run(input)`: Main entry point.
|
| 25 |
+
- `analyze_intent(state)`: Classify user query (Classification/Routing).
|
| 26 |
+
- `execute_tools(state)`: Run selected tools based on intent.
|
| 27 |
+
- `generate_response(state)`: Final synthesis.
|
| 28 |
+
- Uses `strands` components where applicable (e.g., `OpenAIModel` for the cognitive steps).
|
| 29 |
+
|
| 30 |
+
#### [MODIFY] [app.py](file:///Users/christiancontrerascampana/Desktop/GitHub/syf/fraud_model_explainability_assistant/app.py)
|
| 31 |
+
- Import `FraudWorkflow`.
|
| 32 |
+
- Replace `create_enhanced_agent` with `create_workflow` (or update logic).
|
| 33 |
+
- Update `query_agent` to invoke `workflow.run()`.
|
| 34 |
+
|
| 35 |
+
#### [MODIFY] [utils.py](file:///Users/christiancontrerascampana/Desktop/GitHub/syf/fraud_model_explainability_assistant/utils.py)
|
| 36 |
+
- Ensure tools are compatible with direct python execution if needed (they already are decorated with `@tool` but can be called effectively as functions).
|
| 37 |
+
|
| 38 |
+
## Verification Plan
|
| 39 |
+
|
| 40 |
+
### Automated Tests
|
| 41 |
+
- Run `app.py` locally.
|
| 42 |
+
- Use `curl` or the built-in UI to send the example questions:
|
| 43 |
+
- *"Why was application APP-78432 flagged?"* (Should trigger `get_application_summary` + `explain_fraud_score`)
|
| 44 |
+
- *"Check fair lending compliance for APP-55555"* (Should trigger `check_fair_lending_flags`)
|
| 45 |
+
|
| 46 |
+
### Manual Verification
|
| 47 |
+
- Review logs to confirm the step-by-step execution (Intent -> Tool -> Response).
|
| 48 |
+
- Verify the quality of the answers matches or exceeds the previous implementation.
|
docs/multi-agent-pattern-comparison.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Multi-Agent Pattern Comparison
|
| 2 |
+
|
| 3 |
+
This document compares the monolithic "Agent" pattern previously used in the Fraud Model Explainability Assistant with the newly implemented "Workflow" pattern.
|
| 4 |
+
|
| 5 |
+
## Overview
|
| 6 |
+
|
| 7 |
+
The transition from a single, monolithic agent to a structured workflow represents a shift from implicit, LLM-driven control flow to explicit, code-driven orchestration. This is particularly valuable for high-stakes domains like fraud analysis where auditability and reliability are paramount.
|
| 8 |
+
|
| 9 |
+
## Architecture Comparison
|
| 10 |
+
|
| 11 |
+
| Feature | Monolithic Agent (Legacy) | Workflow Pattern (New) |
|
| 12 |
+
| :--- | :--- | :--- |
|
| 13 |
+
| **Control Flow** | **Implicit**: The LLM decides the loop (Reason -> Act -> Observe) entirely. | **Explicit**: Python code defines the steps (Plan -> Execute -> Synthesize). The LLM is a component called within steps. |
|
| 14 |
+
| **Determinism** | **Low**: The agent might skip steps, loop indefinitely, or halluncinate tool calls depending on the prompt. | **High**: The process is guaranteed to follow the defined path. It will always plan first, then execute, then respond. |
|
| 15 |
+
| **Auditability** | **Difficult**: Logs are a mix of thought chains and tool outputs. Hard to programmatically verify if a specific check was performed. | **High**: The `WorkflowState` object captures exactly what intent was classified, which tools were planned, and the result of each. |
|
| 16 |
+
| **Error Handling** | **Fragile**: If a tool fails, the agent might get confused or try to "talk its way out" of the error. | **Robust**: Errors are caught at the step level. The workflow can implement specific fallback logic (e.g., if a tool fails, log it and proceed with partial data). |
|
| 17 |
+
| **Latency** | **Variable**: Depends on how many "thoughts" the agent has. | **Predictable**: Evaluating intent and generating a response are fixed cognitive steps. |
|
| 18 |
+
| **Human-in-the-Loop** | **Complex**: Hard to interrupt the ReAct loop to ask for confirmation. | **Native**: Easy to insert a "wait for approval" step between Planning and Execution. |
|
| 19 |
+
|
| 20 |
+
## Why the Workflow Pattern Wins for Fraud Analysis
|
| 21 |
+
|
| 22 |
+
1. **Regulatory Compliance**: We need to prove that every high-risk application undergoes specific checks (e.g., Fair Lending). A workflow guarantees this step happens; an agent does not.
|
| 23 |
+
2. **Debugging**: When an answer is wrong, we can pinpoint exactly where it failed:
|
| 24 |
+
- Did the Router misclassify the intent?
|
| 25 |
+
- Did the Tool return the wrong data?
|
| 26 |
+
- Did the Response Generator hallucinate?
|
| 27 |
+
3. **Integration**: The workflow is easier to integrate into a larger system (e.g., a credit decisioning pipeline) because it has a predictable input/output contract.
|
docs/walkthrough.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Walkthrough - Fraud Assistant Workflow Pattern
|
| 2 |
+
|
| 3 |
+
I have successfully transitioned the **Fraud Model Explainability Assistant** to a structured **Workflow Pattern**. This change improves determinism, auditability, and reliability by forcing explicit steps for Intent Analysis, Tool Execution, and Response Generation.
|
| 4 |
+
|
| 5 |
+
## Changes
|
| 6 |
+
|
| 7 |
+
### 1. New Workflow Engine ([workflow.py](file:///Users/christiancontrerascampana/Desktop/GitHub/syf/fraud_model_explainability_assistant/workflow.py))
|
| 8 |
+
I created a new `FraudExplainabilityWorkflow` class that orchestrates the conversation:
|
| 9 |
+
- **State Management**: Uses a `WorkflowState` TypedDict to track input, intent, tool calls, and results.
|
| 10 |
+
- **Intent Analysis**: Explicitly plans which tools to call using an LLM router.
|
| 11 |
+
- **Tool Execution**: Systematically executes tools and catches errors per tool.
|
| 12 |
+
- **Async Wrapper**: Implemented specific `async/await` methods for all workflow steps to ensure compatibility with `uvicorn` and `uvloop`, replacing the initial synchronous design.
|
| 13 |
+
|
| 14 |
+
### 2. Application Integration ([app.py](file:///Users/christiancontrerascampana/Desktop/GitHub/syf/fraud_model_explainability_assistant/app.py))
|
| 15 |
+
- Replaced the legacy `Agent` loop with the new `FraudExplainabilityWorkflow`.
|
| 16 |
+
- Added robust dependency handling for `confluence-ingestor` to allow the app to run in lighter environments.
|
| 17 |
+
|
| 18 |
+
### 3. Verification Script ([test_workflow.py](file:///Users/christiancontrerascampana/Desktop/GitHub/syf/fraud_model_explainability_assistant/test_workflow.py))
|
| 19 |
+
- Created a standalone test script to verify the workflow without needing the full web server.
|
| 20 |
+
|
| 21 |
+
## Verification Results
|
| 22 |
+
|
| 23 |
+
### Test Case: "Why was application APP-78432 flagged as high risk?"
|
| 24 |
+
|
| 25 |
+
The workflow successfully:
|
| 26 |
+
1. **Analyzed Intent**: Determined it needed to fetch application summary, fraud score explanation, population comparison, and risk indicators.
|
| 27 |
+
2. **Executed Tools**:
|
| 28 |
+
- `get_application_summary`
|
| 29 |
+
- `explain_fraud_score`
|
| 30 |
+
- `compare_to_population`
|
| 31 |
+
- `check_fair_lending_flags`
|
| 32 |
+
- `get_identity_network`
|
| 33 |
+
3. **Generated Response**: Synthesized all data into a comprehensive explanation.
|
| 34 |
+
|
| 35 |
+
**Log Output:**
|
| 36 |
+
```log
|
| 37 |
+
INFO | workflow | Intent: Analyze why application APP-78432 was flagged..., Tools: 5
|
| 38 |
+
INFO | workflow | Executing get_application_summary with {'application_id': 'APP-78432'}
|
| 39 |
+
INFO | workflow | Executing explain_fraud_score with {'application_id': 'APP-78432'}
|
| 40 |
+
...
|
| 41 |
+
INFO | app | Query completed successfully
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
### Architecture Comparison
|
| 45 |
+
|
| 46 |
+
| Feature | Old Agent | New Workflow |
|
| 47 |
+
| :--- | :--- | :--- |
|
| 48 |
+
| **Control Flow** | Implicit (LLM decides loop) | Explicit (Code defines steps) |
|
| 49 |
+
| **Auditability** | Hard (Mixed logs) | Easy (Structured State logs) |
|
| 50 |
+
| **Robustness** | error-prone tool loops | Per-step error handling |
|
| 51 |
+
| **Dependencies** | Loose | Managed & Robust |
|
| 52 |
+
|
| 53 |
+
## Next Steps
|
| 54 |
+
- The `app.py` is now ready for deployment with the new architecture.
|
| 55 |
+
- You can extend the `WorkflowState` to include user feedback or "Human-in-the-loop" approval steps easily in the future.
|
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,14 +5,13 @@ 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
|
|
|
|
| 5 |
python-multipart>=0.0.6
|
| 6 |
|
| 7 |
# Strands Agents SDK with OpenAI provider support
|
| 8 |
+
strands-agents[openai,anthropic,otel]>=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
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
workflow.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Fraud Model Explainability Assistant - Workflow Implementation
|
| 3 |
+
|
| 4 |
+
This module defines the FraudExplainabilityWorkflow, a structured workflow pattern
|
| 5 |
+
that orchestrates intent classification, tool execution, and response generation
|
| 6 |
+
for the fraud analysis assistant.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import logging
|
| 12 |
+
import asyncio
|
| 13 |
+
from typing import List, Dict, Any, Optional, TypedDict
|
| 14 |
+
from dataclasses import dataclass, field
|
| 15 |
+
|
| 16 |
+
# from strands import Tool # Tool type not directly exported
|
| 17 |
+
from strands.models.openai import OpenAIModel
|
| 18 |
+
|
| 19 |
+
# Import tools
|
| 20 |
+
from utils import (
|
| 21 |
+
get_application_summary,
|
| 22 |
+
explain_fraud_score,
|
| 23 |
+
compare_to_population,
|
| 24 |
+
check_fair_lending_flags,
|
| 25 |
+
get_identity_network,
|
| 26 |
+
get_model_performance,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# Import Confluence tools if available (handled dynamically)
|
| 30 |
+
|
| 31 |
+
logger = logging.getLogger(__name__)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class WorkflowState(TypedDict):
|
| 35 |
+
"""Represents the state of the workflow execution."""
|
| 36 |
+
input_text: str
|
| 37 |
+
messages: List[Dict[str, str]]
|
| 38 |
+
intent: Optional[str]
|
| 39 |
+
tool_calls: List[Dict[str, Any]]
|
| 40 |
+
tool_outputs: List[Dict[str, Any]]
|
| 41 |
+
final_response: Optional[str]
|
| 42 |
+
error: Optional[str]
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class FraudExplainabilityWorkflow:
|
| 46 |
+
"""
|
| 47 |
+
Orchestrates the fraud analysis workflow:
|
| 48 |
+
1. Analyze Intent
|
| 49 |
+
2. Route to Tools
|
| 50 |
+
3. Execute Tools
|
| 51 |
+
4. Generate Response
|
| 52 |
+
"""
|
| 53 |
+
|
| 54 |
+
def __init__(self, model_id: str = "gpt-4o"):
|
| 55 |
+
self.model_id = model_id
|
| 56 |
+
|
| 57 |
+
# Initialize LLM
|
| 58 |
+
openai_api_key = os.environ.get("OPENAI_API_KEY")
|
| 59 |
+
if not openai_api_key:
|
| 60 |
+
logger.warning("OPENAI_API_KEY not found. Workflow will likely fail.")
|
| 61 |
+
|
| 62 |
+
self.llm = OpenAIModel(
|
| 63 |
+
client_args={"api_key": openai_api_key},
|
| 64 |
+
model_id=self.model_id,
|
| 65 |
+
params={"temperature": 0.1, "max_tokens": 2048},
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
# Initialize Tools
|
| 69 |
+
self.tools = self._initialize_tools()
|
| 70 |
+
self.tool_map = {getattr(t, "tool_name", getattr(t, "name", str(t))): t for t in self.tools}
|
| 71 |
+
|
| 72 |
+
def _initialize_tools(self) -> List[Any]:
|
| 73 |
+
"""Initialize and return the list of available tools."""
|
| 74 |
+
tools = [
|
| 75 |
+
get_application_summary,
|
| 76 |
+
explain_fraud_score,
|
| 77 |
+
compare_to_population,
|
| 78 |
+
check_fair_lending_flags,
|
| 79 |
+
get_identity_network,
|
| 80 |
+
get_model_performance,
|
| 81 |
+
]
|
| 82 |
+
|
| 83 |
+
# dynamic import to avoid circular dependency and handle missing deps
|
| 84 |
+
try:
|
| 85 |
+
from app import init_confluence
|
| 86 |
+
from confluence_ingestor.adapters.strands import (
|
| 87 |
+
create_confluence_search_tool,
|
| 88 |
+
create_confluence_loader_tool,
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
rag = init_confluence()
|
| 92 |
+
if rag:
|
| 93 |
+
tools.append(create_confluence_search_tool(rag=rag, k=5))
|
| 94 |
+
tools.append(create_confluence_loader_tool(max_pages=10))
|
| 95 |
+
except ImportError:
|
| 96 |
+
logger.debug("Confluence integration not available (ImportError).")
|
| 97 |
+
except Exception as e:
|
| 98 |
+
logger.error(f"Failed to add Confluence tools: {e}")
|
| 99 |
+
|
| 100 |
+
return tools
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
async def run(self, input_text: str, context_messages: List[Dict[str, str]] = None) -> str:
|
| 104 |
+
"""
|
| 105 |
+
Main entry point for the workflow.
|
| 106 |
+
Executes the steps in order.
|
| 107 |
+
"""
|
| 108 |
+
state: WorkflowState = {
|
| 109 |
+
"input_text": input_text,
|
| 110 |
+
"messages": context_messages or [],
|
| 111 |
+
"intent": None,
|
| 112 |
+
"tool_calls": [],
|
| 113 |
+
"tool_outputs": [],
|
| 114 |
+
"final_response": None,
|
| 115 |
+
"error": None
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
try:
|
| 119 |
+
logger.info(f"Starting workflow for: {input_text}")
|
| 120 |
+
|
| 121 |
+
# Step 1: Analyze Intent & Plan Tools
|
| 122 |
+
await self._analyze_intent_and_plan(state)
|
| 123 |
+
|
| 124 |
+
# Step 2: Execute Tools
|
| 125 |
+
await self._execute_tools(state)
|
| 126 |
+
|
| 127 |
+
# Step 3: Generate Response
|
| 128 |
+
await self._generate_response(state)
|
| 129 |
+
|
| 130 |
+
return state["final_response"]
|
| 131 |
+
|
| 132 |
+
except Exception as e:
|
| 133 |
+
logger.error(f"Workflow execution failed: {e}", exc_info=True)
|
| 134 |
+
return f"I encountered an error processing your request: {str(e)}"
|
| 135 |
+
|
| 136 |
+
async def _call_llm(self, prompt: str) -> str:
|
| 137 |
+
"""Helper to call async LLM."""
|
| 138 |
+
messages = [{"role": "user", "content": [{"text": prompt}]}]
|
| 139 |
+
full_text = ""
|
| 140 |
+
async for chunk in self.llm.stream(messages=messages):
|
| 141 |
+
# Extract text from contentBlockDelta
|
| 142 |
+
if "contentBlockDelta" in chunk:
|
| 143 |
+
delta = chunk["contentBlockDelta"].get("delta", {})
|
| 144 |
+
if "text" in delta:
|
| 145 |
+
full_text += delta["text"]
|
| 146 |
+
return full_text
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
async def _analyze_intent_and_plan(self, state: WorkflowState):
|
| 150 |
+
"""
|
| 151 |
+
Determine the intent and decide which tools to call.
|
| 152 |
+
"""
|
| 153 |
+
prompt = f"""
|
| 154 |
+
You are a routing agent for a Fraud Explainability Assistant.
|
| 155 |
+
Your goal is to analyze the user's request and determine which tools to call.
|
| 156 |
+
|
| 157 |
+
User Request: "{state['input_text']}"
|
| 158 |
+
|
| 159 |
+
Available Tools:
|
| 160 |
+
- get_application_summary(application_id): Basic info about an application.
|
| 161 |
+
- explain_fraud_score(application_id): Detailed SHAP explanations for score.
|
| 162 |
+
- compare_to_population(application_id, comparison_group): Stats vs approved/denied.
|
| 163 |
+
- check_fair_lending_flags(application_id): Compliance check.
|
| 164 |
+
- get_identity_network(application_id): Linkage analysis.
|
| 165 |
+
- get_model_performance(model_name, portfolio): Model metrics.
|
| 166 |
+
- confluence_search(query): Search documentation/policies.
|
| 167 |
+
- confluence_loader(space_key, page_title): Load full doc pages.
|
| 168 |
+
|
| 169 |
+
Return a JSON object with:
|
| 170 |
+
- "intent": Brief description of intent.
|
| 171 |
+
- "tool_calls": List of objects with "tool_name" and "arguments" (dict).
|
| 172 |
+
|
| 173 |
+
If no tool is needed (e.g., greeting), return empty tool_calls.
|
| 174 |
+
"""
|
| 175 |
+
|
| 176 |
+
# Call LLM for planning
|
| 177 |
+
response_text = (await self._call_llm(prompt)).strip()
|
| 178 |
+
|
| 179 |
+
# Clean markdown code blocks if present
|
| 180 |
+
if response_text.startswith("```json"):
|
| 181 |
+
response_text = response_text[7:]
|
| 182 |
+
if response_text.endswith("```"):
|
| 183 |
+
response_text = response_text[:-3]
|
| 184 |
+
|
| 185 |
+
try:
|
| 186 |
+
plan = json.loads(response_text)
|
| 187 |
+
state["intent"] = plan.get("intent", "Unknown")
|
| 188 |
+
state["tool_calls"] = plan.get("tool_calls", [])
|
| 189 |
+
logger.info(f"Intent: {state['intent']}, Tools: {len(state['tool_calls'])}")
|
| 190 |
+
except json.JSONDecodeError:
|
| 191 |
+
logger.error(f"Failed to parse plan JSON: {response_text}")
|
| 192 |
+
state["error"] = "Failed to plan execution."
|
| 193 |
+
|
| 194 |
+
async def _execute_tools(self, state: WorkflowState):
|
| 195 |
+
"""
|
| 196 |
+
Execute the planned tools and store results.
|
| 197 |
+
"""
|
| 198 |
+
for call in state["tool_calls"]:
|
| 199 |
+
tool_name = call["tool_name"]
|
| 200 |
+
args = call.get("arguments", {})
|
| 201 |
+
|
| 202 |
+
if tool_name in self.tool_map:
|
| 203 |
+
try:
|
| 204 |
+
tool_instance = self.tool_map[tool_name]
|
| 205 |
+
logger.info(f"Executing {tool_name} with {args}")
|
| 206 |
+
|
| 207 |
+
# Support both generated tool classes and manual function tools
|
| 208 |
+
if hasattr(tool_instance, "__call__"):
|
| 209 |
+
# Check if tool is async
|
| 210 |
+
if asyncio.iscoroutinefunction(tool_instance):
|
| 211 |
+
result = await tool_instance(**args)
|
| 212 |
+
else:
|
| 213 |
+
result = tool_instance(**args)
|
| 214 |
+
else:
|
| 215 |
+
# Fallback if it's a strands Tool object (depends on implementation)
|
| 216 |
+
# This assumes the tool wrapper handles the call
|
| 217 |
+
pass
|
| 218 |
+
|
| 219 |
+
state["tool_outputs"].append({
|
| 220 |
+
"tool_name": tool_name,
|
| 221 |
+
"result": result
|
| 222 |
+
})
|
| 223 |
+
except Exception as e:
|
| 224 |
+
logger.error(f"Tool {tool_name} failed: {e}")
|
| 225 |
+
state["tool_outputs"].append({
|
| 226 |
+
"tool_name": tool_name,
|
| 227 |
+
"error": str(e)
|
| 228 |
+
})
|
| 229 |
+
else:
|
| 230 |
+
logger.warning(f"Tool {tool_name} not found.")
|
| 231 |
+
|
| 232 |
+
async def _generate_response(self, state: WorkflowState):
|
| 233 |
+
"""
|
| 234 |
+
Synthesize the final answer using tool outputs.
|
| 235 |
+
"""
|
| 236 |
+
context_str = ""
|
| 237 |
+
for output in state["tool_outputs"]:
|
| 238 |
+
if "error" in output:
|
| 239 |
+
context_str += f"\n[Error from {output['tool_name']}]: {output['error']}\n"
|
| 240 |
+
else:
|
| 241 |
+
context_str += f"\n[Result from {output['tool_name']}]:\n{output['result']}\n"
|
| 242 |
+
|
| 243 |
+
if not context_str and not state["tool_calls"]:
|
| 244 |
+
context_str = "[No tools were called. Answer based on general knowledge or conversational context.]"
|
| 245 |
+
|
| 246 |
+
prompt = f"""
|
| 247 |
+
You are a Fraud Model Explainability Assistant.
|
| 248 |
+
|
| 249 |
+
User Request: "{state['input_text']}"
|
| 250 |
+
|
| 251 |
+
Context / Tool Outputs:
|
| 252 |
+
{context_str}
|
| 253 |
+
|
| 254 |
+
Please provide a comprehensive answer to the user's request.
|
| 255 |
+
- Be precise and data-driven.
|
| 256 |
+
- If multiple tools returned data, synthesize them into a coherent narrative.
|
| 257 |
+
- Highlight risk factors and compliance notes if present.
|
| 258 |
+
"""
|
| 259 |
+
|
| 260 |
+
response_text = await self._call_llm(prompt)
|
| 261 |
+
state["final_response"] = response_text
|