ProjectCraft / agents /validate_agent.py
yania-n's picture
fix: coherence check is advisory only - never blocks valid inputs from generating
e495acd
Raw History Blame Contribute Delete
4.12 kB
"""Validate Agent — validates user inputs before any AI call is made. Uses Haiku for semantic coherence checks only."""
import asyncio, json, os, re
import anthropic
from json_repair import repair_json
from pydantic import BaseModel
from typing import Optional
from core.sanitizer import sanitize_objective
from core.date_engine import validate_date_order
from core.planner import ERROR_MESSAGES
class CoherenceResult(BaseModel):
coherent: bool = True
suggestion: Optional[str] = None
# PROMPT v1.0
SYSTEM_PROMPT = """
You are validating a project brief for completeness.
Return ONLY valid JSON:
{ "coherent": true/false, "suggestion": "string or null" }
coherent = true if the objective is specific enough to generate a plan.
suggestion = a short improvement tip if coherent is false, else null.
"""
def _check_required_fields(
objective: str,
teams: list,
start_date: str,
end_date: str,
start_ai: bool,
end_ai: bool,
) -> list[str]:
"""Pure Python field-level checks. Returns list of error strings (empty = OK)."""
errors: list[str] = []
if not objective or not objective.strip():
errors.append(ERROR_MESSAGES["no_objective"])
if not teams or len(teams) < 1:
errors.append(ERROR_MESSAGES["no_teams"])
start_provided = bool(start_date and start_date.strip())
end_provided = bool(end_date and end_date.strip())
if not start_provided and not start_ai and not end_provided and not end_ai:
errors.append(ERROR_MESSAGES["no_dates"])
if start_provided and end_provided and not start_ai and not end_ai:
if not validate_date_order(start_date, end_date):
errors.append(ERROR_MESSAGES["date_order"])
return errors
async def _check_coherence(objective: str) -> CoherenceResult:
"""Call Haiku to check if objective is semantically coherent."""
try:
client = anthropic.AsyncAnthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
response = await client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=256,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": sanitize_objective(objective)}],
)
raw_text = response.content[0].text.strip()
if raw_text.startswith("```"):
first_newline = raw_text.find("\n")
raw_text = raw_text[first_newline + 1:] if first_newline != -1 else raw_text[3:]
if raw_text.rstrip().endswith("```"):
raw_text = raw_text.rstrip()[:-3].rstrip()
try:
data = json.loads(raw_text)
except json.JSONDecodeError:
data = repair_json(raw_text, return_objects=True)
if not isinstance(data, dict):
data = {}
return CoherenceResult(**data)
except anthropic.APITimeoutError:
return CoherenceResult(coherent=True)
except anthropic.RateLimitError:
return CoherenceResult(coherent=True)
except json.JSONDecodeError:
return CoherenceResult(coherent=True)
except Exception:
return CoherenceResult(coherent=True)
async def run(
objective: str,
teams: list,
start_date: str = "",
end_date: str = "",
start_ai: bool = False,
end_ai: bool = False,
) -> dict:
"""
Main entry point. Returns:
{"valid": bool, "errors": list[str], "coherent": bool, "suggestion": str | None}
Step 1: run pure Python field checks
Step 2: if field checks pass, run coherence check
Step 3: assemble result dict
"""
errors = _check_required_fields(objective, teams, start_date, end_date, start_ai, end_ai)
coherence = CoherenceResult(coherent=True)
if not errors:
coherence = await _check_coherence(objective)
# Coherence is advisory only — a low coherence score adds a suggestion but never blocks generation.
# Hard field errors (missing objective/teams/dates) are the only blockers.
return {
"valid": len(errors) == 0,
"errors": errors,
"coherent": coherence.coherent,
"suggestion": coherence.suggestion,
}