chrisjcc's picture
Fraud Assistant Workflow Pattern: 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.
0cb67f9
Raw
History Blame
9.69 kB
"""
Fraud Model Explainability Assistant - Workflow Implementation
This module defines the FraudExplainabilityWorkflow, a structured workflow pattern
that orchestrates intent classification, tool execution, and response generation
for the fraud analysis assistant.
"""
import os
import json
import logging
import asyncio
from typing import List, Dict, Any, Optional, TypedDict
from dataclasses import dataclass, field
# from strands import Tool # Tool type not directly exported
from strands.models.openai import OpenAIModel
# Import tools
from utils import (
get_application_summary,
explain_fraud_score,
compare_to_population,
check_fair_lending_flags,
get_identity_network,
get_model_performance,
)
# Import Confluence tools if available (handled dynamically)
logger = logging.getLogger(__name__)
class WorkflowState(TypedDict):
"""Represents the state of the workflow execution."""
input_text: str
messages: List[Dict[str, str]]
intent: Optional[str]
tool_calls: List[Dict[str, Any]]
tool_outputs: List[Dict[str, Any]]
final_response: Optional[str]
error: Optional[str]
class FraudExplainabilityWorkflow:
"""
Orchestrates the fraud analysis workflow:
1. Analyze Intent
2. Route to Tools
3. Execute Tools
4. Generate Response
"""
def __init__(self, model_id: str = "gpt-4o"):
self.model_id = model_id
# Initialize LLM
openai_api_key = os.environ.get("OPENAI_API_KEY")
if not openai_api_key:
logger.warning("OPENAI_API_KEY not found. Workflow will likely fail.")
self.llm = OpenAIModel(
client_args={"api_key": openai_api_key},
model_id=self.model_id,
params={"temperature": 0.1, "max_tokens": 2048},
)
# Initialize Tools
self.tools = self._initialize_tools()
self.tool_map = {getattr(t, "tool_name", getattr(t, "name", str(t))): t for t in self.tools}
def _initialize_tools(self) -> List[Any]:
"""Initialize and return the list of available tools."""
tools = [
get_application_summary,
explain_fraud_score,
compare_to_population,
check_fair_lending_flags,
get_identity_network,
get_model_performance,
]
# dynamic import to avoid circular dependency and handle missing deps
try:
from app import init_confluence
from confluence_ingestor.adapters.strands import (
create_confluence_search_tool,
create_confluence_loader_tool,
)
rag = init_confluence()
if rag:
tools.append(create_confluence_search_tool(rag=rag, k=5))
tools.append(create_confluence_loader_tool(max_pages=10))
except ImportError:
logger.debug("Confluence integration not available (ImportError).")
except Exception as e:
logger.error(f"Failed to add Confluence tools: {e}")
return tools
async def run(self, input_text: str, context_messages: List[Dict[str, str]] = None) -> str:
"""
Main entry point for the workflow.
Executes the steps in order.
"""
state: WorkflowState = {
"input_text": input_text,
"messages": context_messages or [],
"intent": None,
"tool_calls": [],
"tool_outputs": [],
"final_response": None,
"error": None
}
try:
logger.info(f"Starting workflow for: {input_text}")
# Step 1: Analyze Intent & Plan Tools
await self._analyze_intent_and_plan(state)
# Step 2: Execute Tools
await self._execute_tools(state)
# Step 3: Generate Response
await self._generate_response(state)
return state["final_response"]
except Exception as e:
logger.error(f"Workflow execution failed: {e}", exc_info=True)
return f"I encountered an error processing your request: {str(e)}"
async def _call_llm(self, prompt: str) -> str:
"""Helper to call async LLM."""
messages = [{"role": "user", "content": [{"text": prompt}]}]
full_text = ""
async for chunk in self.llm.stream(messages=messages):
# Extract text from contentBlockDelta
if "contentBlockDelta" in chunk:
delta = chunk["contentBlockDelta"].get("delta", {})
if "text" in delta:
full_text += delta["text"]
return full_text
async def _analyze_intent_and_plan(self, state: WorkflowState):
"""
Determine the intent and decide which tools to call.
"""
prompt = f"""
You are a routing agent for a Fraud Explainability Assistant.
Your goal is to analyze the user's request and determine which tools to call.
User Request: "{state['input_text']}"
Available Tools:
- get_application_summary(application_id): Basic info about an application.
- explain_fraud_score(application_id): Detailed SHAP explanations for score.
- compare_to_population(application_id, comparison_group): Stats vs approved/denied.
- check_fair_lending_flags(application_id): Compliance check.
- get_identity_network(application_id): Linkage analysis.
- get_model_performance(model_name, portfolio): Model metrics.
- confluence_search(query): Search documentation/policies.
- confluence_loader(space_key, page_title): Load full doc pages.
Return a JSON object with:
- "intent": Brief description of intent.
- "tool_calls": List of objects with "tool_name" and "arguments" (dict).
If no tool is needed (e.g., greeting), return empty tool_calls.
"""
# Call LLM for planning
response_text = (await self._call_llm(prompt)).strip()
# Clean markdown code blocks if present
if response_text.startswith("```json"):
response_text = response_text[7:]
if response_text.endswith("```"):
response_text = response_text[:-3]
try:
plan = json.loads(response_text)
state["intent"] = plan.get("intent", "Unknown")
state["tool_calls"] = plan.get("tool_calls", [])
logger.info(f"Intent: {state['intent']}, Tools: {len(state['tool_calls'])}")
except json.JSONDecodeError:
logger.error(f"Failed to parse plan JSON: {response_text}")
state["error"] = "Failed to plan execution."
async def _execute_tools(self, state: WorkflowState):
"""
Execute the planned tools and store results.
"""
for call in state["tool_calls"]:
tool_name = call["tool_name"]
args = call.get("arguments", {})
if tool_name in self.tool_map:
try:
tool_instance = self.tool_map[tool_name]
logger.info(f"Executing {tool_name} with {args}")
# Support both generated tool classes and manual function tools
if hasattr(tool_instance, "__call__"):
# Check if tool is async
if asyncio.iscoroutinefunction(tool_instance):
result = await tool_instance(**args)
else:
result = tool_instance(**args)
else:
# Fallback if it's a strands Tool object (depends on implementation)
# This assumes the tool wrapper handles the call
pass
state["tool_outputs"].append({
"tool_name": tool_name,
"result": result
})
except Exception as e:
logger.error(f"Tool {tool_name} failed: {e}")
state["tool_outputs"].append({
"tool_name": tool_name,
"error": str(e)
})
else:
logger.warning(f"Tool {tool_name} not found.")
async def _generate_response(self, state: WorkflowState):
"""
Synthesize the final answer using tool outputs.
"""
context_str = ""
for output in state["tool_outputs"]:
if "error" in output:
context_str += f"\n[Error from {output['tool_name']}]: {output['error']}\n"
else:
context_str += f"\n[Result from {output['tool_name']}]:\n{output['result']}\n"
if not context_str and not state["tool_calls"]:
context_str = "[No tools were called. Answer based on general knowledge or conversational context.]"
prompt = f"""
You are a Fraud Model Explainability Assistant.
User Request: "{state['input_text']}"
Context / Tool Outputs:
{context_str}
Please provide a comprehensive answer to the user's request.
- Be precise and data-driven.
- If multiple tools returned data, synthesize them into a coherent narrative.
- Highlight risk factors and compliance notes if present.
"""
response_text = await self._call_llm(prompt)
state["final_response"] = response_text