Why Agents and Vector Search?
Most AI applications today use a pattern called RAG (Retrieval-Augmented Generation), where the system fetches data and rigidly stuffs it into an LLM prompt. This project uses a more advanced pattern: a ReAct (Reasoning and Acting) Agent.
-
What is an Agent? Instead of just answering a prompt, an Agent is given a goal and a set of “tools” (Python functions). It enters a loop: it reasons about what to do, it acts by calling a tool, it observes the result, and it repeats until it solves the problem.
-
What is Vector Search (
pgvector)? Machine learning models can convert text or user behaviors into “embeddings” lists of thousands of numbers representing coordinates in a high-dimensional space. Concepts or behaviors that are similar end up physically close to each other in this space. By storing these embeddings in PostgreSQL using thepgvectorextension, we can query the database not for exact matches, but for mathematical similarity.
Building the Tools (Giving the LLM Hands)
For the Gemini model to investigate fraud, it needs hands to fetch data. I created two specific tools using LangChain’s @tool decorator. The docstrings here are critical; they are not just for developers the LLM actually reads them to understand when and how to use the function.
The most complex tool is the historical vector search. If a transaction doesn’t have an embedding yet, the tool generates one “Just-In-Time” (JIT) and searches the database for similar behavioral footprints
# src/agent/tools.py (Simplified)
@tool
def search_historical_fraud(transaction_id: str) -> str:
"""
Searches the database for past transactions that are mathematically/behaviorally
similar to the target transaction using vector similarity.
"""
record = get_transaction_by_id(transaction_id)
vector_to_search = record.get("embedding")
# Just-In-Time Embedding Generation if missing
if not vector_to_search:
embedder = GoogleGenerativeAIEmbeddings(model="gemini-embedding-001")
text_to_embed = f"Transaction amount: ${tx_data.get('Amount')}, Time: {tx_data.get('Time')}."
vector_to_search = embedder.embed_query(text_to_embed)
update_transaction_embedding(transaction_id, vector_to_search)
# Perform Vector Search
similar_records = search_similar_transactions(vector_to_search, exclude_id=transaction_id, limit=3)
# ... formats the result into a string for the LLM to read ...
Under the hood, this tool relies on a brilliant feature of pgvector. Instead of writing complex matching algorithms, we can use the <=> operator directly in our SQL query to calculate the Cosine Distance between vectors:
# src/core/database.py - Inside search_similar_transactions()
# The <=> operator calculates Cosine Distance natively in PostgreSQL
query = text("""
SELECT transaction_id, is_fraud, amount, embedding <=> :target_vector AS distance
FROM predictions
WHERE transaction_id != :target_id AND embedding IS NOT NULL
ORDER BY distance ASC
LIMIT :limit
""")
The LangGraph Agent Loop
With the tools ready, we initialize the agent using LangGraph. We provide the Gemini 2.5 Flash model with its tools and a strict system prompt.
Because the raw PCA-transformed features (V1 through V28) from the dataset are anonymized, the prompt explicitly instructs the LLM not to hallucinate their meanings, ensuring the final report remains highly professional.
# src/agent/agent.py (Simplified)
def run_investigation(transaction_id: str) -> dict:
# 1. Setup the Agentic Loop
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0)
tools = [analyze_shap_values, search_historical_fraud]
agent_executor = create_react_agent(llm, tools)
# 2. Provide strict operational boundaries
prompt = f"""
You are an expert financial risk analyst. Investigate transaction {transaction_id}.
INSTRUCTIONS:
1. Use the SHAP tool to find out why the model flagged it.
2. Use the historical search tool to see if this pattern has happened before.
3. DATA RULE: Features V1-V28 are PCA-transformed hidden behavioral vectors.
Do NOT guess what they mean. Refer to them as 'anomalous behavioral vectors'.
"""
# 3. Execute the Loop
for step_event in agent_executor.stream({"messages": [("user", prompt)]}):
pass # The agent is thinking, using tools, and observing here
raw_final_message = list(step_event.values())[0]["messages"][-1].content
# 4. Enforce Structured Output
# We pass the raw text through a structured output chain to guarantee a JSON format
# that our Streamlit UI can render perfectly every time.
structured_llm = llm.with_structured_output(InvestigationReport)
final_report = structured_llm.invoke(f"Extract info: {raw_final_message}")
return json.loads(final_report.model_dump_json())
By combining Explainable AI (SHAP) with Vector Search (pgvector) under the direction of an autonomous LangGraph agent, the system transforms raw database rows into actionable, boardroom ready intelligence in seconds