fund-flow-backend / src /copilot /agents /graph_analysis.py
Aniket2006's picture
feat(copilot): complete copilot integration and resolve CSS styling compliance
f70ac6a
Raw
History Blame Contribute Delete
7.45 kB
"""
GraphAnalysisAgent - Network pattern detection
Analyzes account network: peers, cycles, communities
"""
from typing import Dict, List
from .base import Agent, AgentConfig, AgentResult
import time
class GraphAnalysisAgent(Agent):
"""
Analyzes transaction network around an account
Direct graph operations (no LLM needed for retrieval)
"""
def __init__(self, api_pool, app_state=None):
config = AgentConfig(
name="GraphAnalysisAgent",
model="llama-3.1-8b-instant", # llama-3.1-70b-versatile deprecated
temperature=0.2,
max_tokens=1000,
timeout_ms=10000,
)
super().__init__(config, api_pool)
self.app_state = app_state
def set_app_state(self, app_state):
self.app_state = app_state
def _build_prompt(self, **inputs) -> str:
return ""
def _parse_response(self, response_text: str) -> Dict:
return {}
async def invoke(self, account_id: str = None, hops: int = 2, max_nodes: int = 30, **kwargs) -> AgentResult:
"""
Analyze network around account
Returns: peers, cycles, community info, network metrics
"""
start_time = time.time()
if not self.app_state or not account_id:
return await self._create_result(
success=False,
data={},
error="No account_id or app_state",
start_time=start_time,
)
try:
data = {
"account_id": account_id,
"network_metrics": {},
"peers": [],
"cycles": [],
"community": {},
"subgraph_summary": {},
}
if not hasattr(self.app_state, "graph") or self.app_state.graph is None:
return await self._create_result(
success=False,
data=data,
error="Graph not available",
start_time=start_time,
)
graph = self.app_state.graph
if account_id not in graph.nodes():
return await self._create_result(
success=True,
data=data,
start_time=start_time,
)
# Get network metrics
data["network_metrics"] = {
"in_degree": graph.in_degree(account_id),
"out_degree": graph.out_degree(account_id),
"total_degree": graph.degree(account_id),
}
# Add centrality scores if available
if hasattr(self.app_state, "pagerank_scores"):
data["network_metrics"]["pagerank"] = self.app_state.pagerank_scores.get(account_id, 0)
if hasattr(self.app_state, "betweenness_scores"):
data["network_metrics"]["betweenness"] = self.app_state.betweenness_scores.get(account_id, 0)
# Community info
if hasattr(self.app_state, "louvain_partition"):
community_id = self.app_state.louvain_partition.get(account_id)
if community_id is not None:
community_members = [
node for node, comm in self.app_state.louvain_partition.items()
if comm == community_id
]
fraud_in_community = sum(
1 for node in community_members
if self.app_state.features_by_account.get(node, {}).get("fraud_flag", 0)
)
data["community"] = {
"community_id": community_id,
"size": len(community_members),
"fraud_count": fraud_in_community,
"fraud_rate": fraud_in_community / max(1, len(community_members)),
}
# Get direct peers (1-hop)
successors = list(graph.successors(account_id))[:10]
predecessors = list(graph.predecessors(account_id))[:10]
peers = []
for peer_id in set(successors + predecessors):
peer_features = self.app_state.features_by_account.get(peer_id, {})
peer_data = {
"account_id": peer_id,
"relationship": [],
"risk_score": peer_features.get("risk_score", 0),
"is_fraud": peer_features.get("fraud_flag", 0) == 1,
}
if peer_id in successors:
peer_data["relationship"].append("sends_to")
if peer_id in predecessors:
peer_data["relationship"].append("receives_from")
peers.append(peer_data)
# Sort peers by risk
peers.sort(key=lambda x: x["risk_score"], reverse=True)
data["peers"] = peers[:10]
# Detect cycles (simple cycle detection)
cycles = self._detect_cycles(graph, account_id, max_length=5)
data["cycles"] = cycles[:5]
# Subgraph summary
try:
from src.graph_builder import get_subgraph
subgraph = get_subgraph(graph, account_id, hops=hops, max_nodes=max_nodes)
data["subgraph_summary"] = {
"nodes_count": subgraph.number_of_nodes(),
"edges_count": subgraph.number_of_edges(),
"depth": hops,
}
except Exception as e:
self.logger.warning(f"Subgraph extraction failed: {e}")
tokens_estimate = self._estimate_tokens(str(data))
self.logger.info(
f"[OK] {self.config.name}: Analyzed {account_id} "
f"({len(peers)} peers, {len(cycles)} cycles, ~{tokens_estimate} tokens)"
)
return await self._create_result(
success=True,
data=data,
tokens_output=tokens_estimate,
start_time=start_time,
)
except Exception as e:
self.logger.error(f"[FAIL] {self.config.name}: {e}")
return await self._create_result(
success=False,
data={},
error=str(e),
start_time=start_time,
)
def _detect_cycles(self, graph, account_id: str, max_length: int = 5) -> List[Dict]:
"""Detect cycles involving the account (simplified)"""
cycles = []
try:
import networkx as nx
# Get small subgraph for cycle detection (performance)
try:
from src.graph_builder import get_subgraph
subgraph = get_subgraph(graph, account_id, hops=3, max_nodes=50)
except Exception:
subgraph = graph
# Find simple cycles containing account_id
simple_cycles = list(nx.simple_cycles(subgraph))
for cycle in simple_cycles[:5]:
if account_id in cycle and len(cycle) <= max_length:
cycles.append({
"cycle": cycle + [cycle[0]], # Close the cycle for display
"length": len(cycle),
})
except Exception as e:
self.logger.warning(f"Cycle detection failed: {e}")
return cycles