Agents
The AI Ingredient Scanner uses four specialized agents, each handling a specific aspect of the analysis pipeline.
Agent Overviewโ
Supervisor Agentโ
File: agents/supervisor.py
The Supervisor orchestrates the workflow by determining the next step based on current state.
Routing Logicโ
def route_next(state: WorkflowState) -> RouteType:
"""Determine the next node in the workflow."""
# Check for errors
if state.get("error"):
return NODE_END
# Step 1: Need research data?
if not has_research_data(state):
return NODE_RESEARCH
# Step 2: Need analysis report?
if not has_analysis_report(state):
return NODE_ANALYSIS
# Step 3: Check validation status
critic_feedback = state.get("critic_feedback")
if critic_feedback is None:
return NODE_CRITIC
# Step 4: Handle validation results
if is_approved(state):
return NODE_END
if is_escalated(state):
return NODE_END
if is_rejected(state):
return NODE_ANALYSIS # Retry
return NODE_END
Route Typesโ
| Route | Condition | Action |
|---|---|---|
research | No ingredient data | Fetch ingredient info |
analysis | No report generated | Generate safety report |
critic | Report needs validation | Validate quality |
end | Complete or error | Finish workflow |
Research Agentโ
File: agents/research.py
The Research Agent fetches ingredient safety data using a dual-source strategy.
Architectureโ
Dual-Source Strategyโ
def _research_single_ingredient(ingredient_name: str) -> IngredientData | None:
# Try vector database first
result = lookup_ingredient(ingredient_name)
if result and result["confidence"] >= CONFIDENCE_THRESHOLD:
return result # High confidence match
# Fall back to Google Search
grounded_result = grounded_ingredient_search(ingredient_name)
if grounded_result:
_save_to_qdrant(grounded_result) # Learn for next time
return grounded_result
return None # Will use unknown record
Parallel Processingโ
For lists with more than 3 ingredients, research is parallelized:
BATCH_SIZE = 3 # Ingredients per worker
def _research_parallel(ingredients: list[str]) -> list[IngredientData]:
batches = _create_batches(ingredients, BATCH_SIZE)
with ThreadPoolExecutor(max_workers=len(batches)) as executor:
futures = {
executor.submit(_research_batch, idx, batch): idx
for idx, batch in enumerate(batches)
}
for future in as_completed(futures):
batch_results = future.result()
results_by_batch[futures[future]] = batch_results
return _reassemble_results(results_by_batch)
Output Schemaโ
class IngredientData(TypedDict):
name: str
purpose: str
safety_rating: int # 1-10 scale
concerns: str
recommendation: str # SAFE/CAUTION/AVOID
allergy_risk_flag: str # HIGH/LOW
origin: str # Natural/Synthetic
category: str # Food/Cosmetics/Both
regulatory_status: str
source: str # qdrant/google_search
confidence: float
Analysis Agentโ
File: agents/analysis.py
The Analysis Agent generates personalized safety reports using Gemini 2.0 Flash.
Personalization Factorsโ
| Factor | Impact |
|---|---|
| Allergies | Prominent warnings for matches |
| Skin Type | Targeted recommendations |
| Expertise | Beginner (simple) / Expert (technical) tone |
Report Generationโ
Tone Adaptationโ
TONE_INSTRUCTIONS = {
"beginner": """
Use simple, everyday language.
Avoid technical jargon.
Explain concepts clearly for someone new to ingredient analysis.
""",
"expert": """
Use technical terminology when appropriate.
Include chemical details and concentrations.
Reference regulatory standards and scientific studies.
"""
}
Risk Calculationโ
def _parse_llm_overall_risk(llm_analysis: str) -> tuple[RiskLevel, int]:
"""Determine overall risk from analysis."""
# Rule 1: Any AVOID โ HIGH risk
if has_avoid_recommendation(llm_analysis):
return RiskLevel.HIGH, avg_rating
# Rule 2: Any banned ingredient โ HIGH risk
if has_regulatory_ban(llm_analysis):
return RiskLevel.HIGH, avg_rating
# Rule 3: Calculate from average
if avg_rating <= 3:
return RiskLevel.HIGH, avg_rating
elif avg_rating <= 6:
return RiskLevel.MEDIUM, avg_rating
else:
return RiskLevel.LOW, avg_rating
Critic Agentโ
File: agents/critic.py
The Critic Agent validates report quality using a comprehensive 5-gate system.
5-Gate Validationโ
Gate Detailsโ
| Gate | Check | Pass Criteria |
|---|---|---|
| Completeness | All ingredients addressed | 8/9 ingredients covered |
| Format | Markdown table structure | Valid table with columns |
| Allergen Match | User allergies flagged | Matching ingredients warned |
| Consistency | Ratings match concerns | No contradictions |
| Tone | Appropriate for expertise | Matches beginner/expert |
Validation Responseโ
class CriticFeedback(TypedDict):
result: ValidationResult # APPROVED/REJECTED/ESCALATED
completeness_ok: bool
format_ok: bool
allergens_ok: bool
consistency_ok: bool
tone_ok: bool
feedback: str
failed_gates: list[str]
Retry Logicโ
def validate_report(state: WorkflowState) -> dict:
# Run validation
validation_result = _run_multi_gate_validation(report, ...)
all_gates_passed = all([
validation_result["completeness_ok"],
validation_result["format_ok"],
validation_result["allergens_ok"],
validation_result["consistency_ok"],
validation_result["tone_ok"],
])
if all_gates_passed:
result = ValidationResult.APPROVED
elif retry_count >= max_retries:
result = ValidationResult.ESCALATED
else:
result = ValidationResult.REJECTED
new_retry_count = retry_count + 1