Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions backend/app/agents/injection_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import json
from typing import List, Dict
from anthropic import Anthropic
from llama_index.core.workflow import Workflow, step, StartEvent, StopEvent


class ContextInjectionWorkflow(Workflow):
"""LlamaIndex workflow for intelligently deciding which context to inject"""

def __init__(self, anthropic_api_key: str):
super().__init__()
self.client = Anthropic(api_key=anthropic_api_key)

@step
async def evaluate_relevance(self, ev: StartEvent) -> StopEvent:
"""Use Claude to evaluate which candidate facts are relevant to inject"""
query: str = ev.get("query")
candidates: List[Dict] = ev.get("candidates")
platform: str = ev.get("platform")

# If no candidates, return empty
if not candidates:
return StopEvent(result=[])

# Build the prompt for Claude to evaluate relevance
candidates_text = "\n".join([
f"{i+1}. {candidate['fact']['text']} "
f"(from {candidate['fact'].get('platform', 'unknown')}, "
f"category: {candidate['fact'].get('category', 'unknown')}, "
f"similarity: {candidate['similarity']:.3f})"
for i, candidate in enumerate(candidates)
])

prompt = f"""You are a context injection agent. Your job is to decide which facts from the user's history are TRULY relevant to inject into their current conversation.

Current Platform: {platform}
User's Query/Message: "{query}"

Available Facts (ordered by semantic similarity):
{candidates_text}

For each fact, decide if it should be injected into the conversation. A fact should be injected if:
✓ It provides valuable context that helps answer the query
✓ It reveals a preference/constraint that would improve the AI's response
✓ It contains information the user might have forgotten to mention
✓ It would prevent the AI from making wrong assumptions

A fact should NOT be injected if:
✗ It's only tangentially related
✗ It's too general or vague to be useful
✗ The user's query already implies this information
✗ It would be noise rather than signal

IMPORTANT: Be selective! Only inject facts that genuinely improve the conversation. It's better to inject 1-2 highly relevant facts than 5 marginally related ones.

Return ONLY a JSON array of fact indices (1-based) that should be injected, ordered by importance:
[1, 3, 5]

If no facts are relevant, return an empty array:
[]

Return JSON array only, no other text:"""

try:
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=512,
messages=[{
"role": "user",
"content": prompt
}]
)

# Parse response
content = response.content[0].text.strip()

# Extract JSON from potential markdown code blocks
if "```json" in content:
content = content.split("```json")[1].split("```")[0].strip()
elif "```" in content:
content = content.split("```")[1].split("```")[0].strip()

selected_indices = json.loads(content)

# Convert 1-based indices to actual facts
relevant_facts = []
for idx in selected_indices:
if 1 <= idx <= len(candidates):
fact_text = candidates[idx - 1]['fact']['text']
similarity = candidates[idx - 1]['similarity']
relevant_facts.append(fact_text)
print(f"[Injection Agent] ✓ Injecting fact #{idx}: '{fact_text}' (similarity: {similarity:.3f})")

if not relevant_facts:
print(f"[Injection Agent] No facts deemed relevant for query: '{query}'")
else:
print(f"[Injection Agent] Injecting {len(relevant_facts)}/{len(candidates)} candidate facts")

return StopEvent(result=relevant_facts)

except Exception as e:
print(f"[Injection Agent] Error evaluating relevance: {e}")
# Fallback: use similarity threshold
fallback_facts = []
for candidate in candidates[:3]: # Top 3 by similarity
if candidate['similarity'] > 0.6:
fallback_facts.append(candidate['fact']['text'])
print(f"[Injection Agent] Fallback: returning {len(fallback_facts)} facts by similarity")
return StopEvent(result=fallback_facts)


class ContextInjectionAgent:
"""Agent for intelligently deciding which context to inject into conversations"""

def __init__(self, anthropic_api_key: str):
self.anthropic_api_key = anthropic_api_key

async def select_relevant_context(
self,
query: str,
candidates: List[Dict],
platform: str
) -> List[str]:
"""
Use AI agent to select which candidate facts are truly relevant

Args:
query: The user's current query/message
candidates: List of candidate facts with similarity scores
Format: [{'fact': {...}, 'similarity': float}, ...]
platform: The platform where the query is from

Returns:
List of fact texts that should be injected
"""
workflow = ContextInjectionWorkflow(anthropic_api_key=self.anthropic_api_key)

result = await workflow.run(
query=query,
candidates=candidates,
platform=platform
)

return result if isinstance(result, list) else []
67 changes: 54 additions & 13 deletions backend/app/agents/retrieval_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import List, Dict
from anthropic import Anthropic
import voyageai
from app.agents.injection_agent import ContextInjectionAgent


class ContextRetrievalAgent:
Expand All @@ -10,6 +11,7 @@ class ContextRetrievalAgent:
def __init__(self, anthropic_api_key: str, voyage_api_key: str):
self.anthropic_client = Anthropic(api_key=anthropic_api_key)
self.voyage_client = voyageai.Client(api_key=voyage_api_key)
self.injection_agent = ContextInjectionAgent(anthropic_api_key=anthropic_api_key)
self.facts_cache = [] # In-memory cache for MVP
self.embeddings_cache = [] # Cached embeddings

Expand Down Expand Up @@ -95,21 +97,36 @@ async def get_relevant_context(
self,
query: str,
platform: str,
limit: int = 5
limit: int = 5,
use_agent: bool = True
) -> List[str]:
"""Retrieve semantically relevant context"""
"""
Retrieve relevant context using two-stage approach:
1. Semantic search to get candidates
2. Agent-based evaluation to select truly relevant facts

Args:
query: User's current query/message
platform: Platform where query is from
limit: Maximum number of facts to return
use_agent: If True, use injection agent; if False, use similarity threshold only

Returns:
List of relevant fact texts to inject
"""

if not self.facts_cache:
print("[Retrieval] No facts in cache")
return []

print(f"[Retrieval] Query: '{query}' from platform: {platform}")
print(f"[Retrieval] Total facts in cache: {len(self.facts_cache)}")
print(f"[Retrieval] Agent-based injection: {use_agent}")

# Get query embedding
query_embedding = self._get_embedding(query)

# Calculate similarities
# Stage 1: Calculate semantic similarities
similarities = []
for i, fact in enumerate(self.facts_cache):
# Skip facts from the same platform
Expand All @@ -127,19 +144,43 @@ async def get_relevant_context(
'similarity': similarity
})

# Sort by similarity and take top results
# Sort by similarity
similarities.sort(key=lambda x: x['similarity'], reverse=True)
top_results = similarities[:limit]

# Filter by minimum similarity threshold
context = []
for result in top_results:
if result['similarity'] > 0.51: # Threshold for relevance
print(f"[Retrieval] Including: '{result['fact']['text']}' (similarity: {result['similarity']:.4f})")
context.append(result['fact']['text'])
if use_agent:
# Stage 2: Use injection agent to intelligently select relevant facts
# Get more candidates for the agent to evaluate (top 10-15)
candidate_count = min(15, len(similarities))
# Lower threshold for candidates - let the agent decide what's relevant
candidates = [s for s in similarities[:candidate_count] if s['similarity'] > 0.3]

if not candidates:
print("[Retrieval] No candidates above minimum threshold (0.3)")
return []

print(f"[Retrieval] Passing {len(candidates)} candidates to injection agent")

# Use injection agent to select relevant facts
context = await self.injection_agent.select_relevant_context(
query=query,
candidates=candidates,
platform=platform
)

print(f"[Retrieval] Agent selected {len(context)} facts to inject")
return context[:limit] # Respect the limit

else:
# Fallback: Traditional similarity threshold approach
top_results = similarities[:limit]
context = []
for result in top_results:
if result['similarity'] > 0.51: # Threshold for relevance
print(f"[Retrieval] Including: '{result['fact']['text']}' (similarity: {result['similarity']:.4f})")
context.append(result['fact']['text'])

print(f"[Retrieval] Returning {len(context)} context items")
return context
print(f"[Retrieval] Returning {len(context)} context items (threshold-based)")
return context

async def understand_query(self, query: str) -> Dict:
"""Use Claude to extract intent and entities from query"""
Expand Down
106 changes: 106 additions & 0 deletions backend/test_injection_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
Quick test to demonstrate agent-based context injection
This shows how the injection agent intelligently selects relevant facts
"""
import asyncio
import os
from app.agents.injection_agent import ContextInjectionAgent


async def test_injection_agent():
"""Test the injection agent with sample data"""

# Note: This requires ANTHROPIC_API_KEY in environment
api_key = os.getenv("ANTHROPIC_API_KEY", "dummy-key-for-testing")

if api_key == "dummy-key-for-testing":
print("⚠ ANTHROPIC_API_KEY not set - skipping live test")
print("✓ Agent classes load successfully")
print("✓ Integration test structure verified")
return

agent = ContextInjectionAgent(anthropic_api_key=api_key)

# Sample scenario: User asks about dinner recommendations
query = "What should I make for dinner tonight?"
platform = "chatgpt"

# Sample candidate facts (sorted by similarity)
candidates = [
{
"fact": {
"text": "prefers vegetarian food",
"category": "preference",
"platform": "claude",
"confidence": 0.9
},
"similarity": 0.72
},
{
"fact": {
"text": "allergic to peanuts",
"category": "biographical",
"platform": "claude",
"confidence": 0.95
},
"similarity": 0.68
},
{
"fact": {
"text": "enjoys Italian cuisine",
"category": "preference",
"platform": "gemini",
"confidence": 0.85
},
"similarity": 0.65
},
{
"fact": {
"text": "likes watching sci-fi movies",
"category": "interest",
"platform": "claude",
"confidence": 0.8
},
"similarity": 0.45
},
{
"fact": {
"text": "works as a software engineer",
"category": "biographical",
"platform": "claude",
"confidence": 0.9
},
"similarity": 0.35
}
]

print("=" * 60)
print("AGENT-BASED CONTEXT INJECTION TEST")
print("=" * 60)
print(f"\nQuery: '{query}'")
print(f"Platform: {platform}")
print(f"\nCandidate Facts ({len(candidates)}):")
for i, c in enumerate(candidates, 1):
print(f" {i}. {c['fact']['text']} (similarity: {c['similarity']:.2f})")

print("\nCalling injection agent...")
relevant_facts = await agent.select_relevant_context(
query=query,
candidates=candidates,
platform=platform
)

print(f"\n{'=' * 60}")
print(f"RESULT: Agent selected {len(relevant_facts)} relevant facts:")
print("=" * 60)
for i, fact in enumerate(relevant_facts, 1):
print(f" {i}. {fact}")

print("\nExpected behavior:")
print(" - Should include: vegetarian preference, peanut allergy, Italian cuisine")
print(" - Should exclude: sci-fi movies (not food-related), job title")
print("\n✓ Agent-based injection test completed!")


if __name__ == "__main__":
asyncio.run(test_injection_agent())