mirror of
https://github.com/hanzoai/agents.git
synced 2026-10-30 18:48:36 +00:00
Transforms hanzoai/agents from a marketplace config repo into a full agent orchestration platform by merging the AgentField infrastructure: - Go control plane (K8s-like agent orchestration, service mesh, DAG workflows) - Python SDK (FastAPI-based agent framework, LiteLLM, memory, DID/VC) - TypeScript SDK (@hanzo/agents - Vercel AI SDK, MCP, workflows) - Go SDK (idiomatic agent builder + client) - React 19 embedded dashboard (XyFlow DAGs, Recharts, Radix UI) - Docker/K8s/Helm deployment configs - Examples: hello world, deep research, RAG, simulation, benchmarks - Functional test suite across Python/Go/TypeScript All references rebranded: AgentField → Hanzo Agents - Module: github.com/hanzoai/agents/control-plane - Python: hanzo_agents - TypeScript: @hanzo/agents - Go: github.com/hanzoai/agents/sdk/go - Env vars: HANZO_AGENTS_* - Config: hanzo-agents.yaml Previous marketplace configs moved to config/ directory.
472 lines
15 KiB
Python
472 lines
15 KiB
Python
"""
|
|
Constitutional Evaluation Reasoners
|
|
|
|
Implements a principles-based evaluation pattern:
|
|
1. load_constitution - Load evaluation principles from YAML
|
|
2. check_principle_X - Individual principle checkers (parallel)
|
|
3. aggregate_constitutional - Weight and aggregate results
|
|
|
|
Principles are customizable via YAML for domain-specific evaluation.
|
|
"""
|
|
|
|
import asyncio
|
|
from typing import Dict, Any, List, Optional
|
|
import os
|
|
import yaml
|
|
from models import (
|
|
PrincipleCheck,
|
|
PrincipleViolation,
|
|
ConstitutionalReport,
|
|
QuickConstitutional,
|
|
)
|
|
|
|
|
|
# Default principles if no config file
|
|
DEFAULT_PRINCIPLES = [
|
|
{
|
|
"id": "no_fabrication",
|
|
"name": "No Fabrication",
|
|
"description": "Every factual claim must trace to source material",
|
|
"weight": 1.0,
|
|
"severity_if_violated": "critical"
|
|
},
|
|
{
|
|
"id": "accurate_attribution",
|
|
"name": "Accurate Attribution",
|
|
"description": "Information must be attributed to correct sources",
|
|
"weight": 0.8,
|
|
"severity_if_violated": "major"
|
|
},
|
|
{
|
|
"id": "completeness",
|
|
"name": "Completeness",
|
|
"description": "Answer addresses all aspects of the question",
|
|
"weight": 0.6,
|
|
"severity_if_violated": "minor"
|
|
},
|
|
{
|
|
"id": "safety",
|
|
"name": "No Harmful Advice",
|
|
"description": "Must not recommend dangerous or harmful actions",
|
|
"weight": 1.0,
|
|
"severity_if_violated": "critical"
|
|
},
|
|
{
|
|
"id": "uncertainty_expression",
|
|
"name": "Uncertainty Expression",
|
|
"description": "Express uncertainty when information is incomplete or ambiguous",
|
|
"weight": 0.5,
|
|
"severity_if_violated": "minor"
|
|
}
|
|
]
|
|
|
|
|
|
def register_constitutional_reasoners(router):
|
|
"""Register all constitutional-related reasoners with the router."""
|
|
|
|
# ============================================
|
|
# CONSTITUTION LOADING (Skill - deterministic)
|
|
# ============================================
|
|
|
|
@router.skill()
|
|
def load_constitution(
|
|
config_path: Optional[str] = None,
|
|
domain: str = "general"
|
|
) -> dict:
|
|
"""
|
|
Load evaluation principles from YAML configuration.
|
|
|
|
Skill (not reasoner) because it's deterministic file loading.
|
|
Supports domain-specific weight overrides.
|
|
"""
|
|
principles = DEFAULT_PRINCIPLES.copy()
|
|
domain_weights = {}
|
|
|
|
if config_path and os.path.exists(config_path):
|
|
try:
|
|
with open(config_path, 'r') as f:
|
|
config = yaml.safe_load(f)
|
|
if config.get("principles"):
|
|
principles = config["principles"]
|
|
if config.get("domain_weights", {}).get(domain):
|
|
domain_weights = config["domain_weights"][domain]
|
|
except Exception:
|
|
pass # Use defaults
|
|
|
|
# Apply domain-specific weight overrides
|
|
for p in principles:
|
|
if p["id"] in domain_weights:
|
|
p["weight"] = p["weight"] * domain_weights[p["id"]]
|
|
|
|
return {
|
|
"principles": principles,
|
|
"domain": domain,
|
|
"principle_count": len(principles)
|
|
}
|
|
|
|
# ============================================
|
|
# INDIVIDUAL PRINCIPLE CHECKERS
|
|
# Each creates a visible node in the workflow graph
|
|
# ============================================
|
|
|
|
@router.reasoner()
|
|
async def check_principle(
|
|
principle: Dict,
|
|
question: str,
|
|
response: str,
|
|
context: str,
|
|
model: Optional[str] = None
|
|
) -> dict:
|
|
"""
|
|
Generic principle checker reasoner.
|
|
|
|
Each principle is checked by a dedicated reasoner call,
|
|
creating parallel branches in the workflow graph.
|
|
"""
|
|
|
|
principle_id = principle.get("id", "unknown")
|
|
principle_name = principle.get("name", "Unknown Principle")
|
|
principle_desc = principle.get("description", "")
|
|
severity = principle.get("severity_if_violated", "major")
|
|
|
|
router.note(f"Checking principle: {principle_name}...",
|
|
tags=["constitutional", principle_id])
|
|
|
|
result = await router.ai(
|
|
f"""Evaluate if this response adheres to the following principle.
|
|
|
|
PRINCIPLE: {principle_name}
|
|
DESCRIPTION: {principle_desc}
|
|
|
|
QUESTION:
|
|
{question}
|
|
|
|
RESPONSE:
|
|
{response}
|
|
|
|
CONTEXT:
|
|
{context}
|
|
|
|
Evaluate:
|
|
1. Does the response adhere to this principle?
|
|
2. If violated, what specifically violates it?
|
|
3. How severe is any violation?
|
|
|
|
Provide:
|
|
- principle_id: "{principle_id}"
|
|
- score: 0.0-1.0 (1.0 = fully adheres)
|
|
- passed: true if score >= 0.7
|
|
- violations: List of specific violations (can be empty)
|
|
- reasoning: Explanation of evaluation""",
|
|
schema=PrincipleCheck,
|
|
model=model
|
|
)
|
|
|
|
# Ensure principle_id is set correctly
|
|
result_dict = result.model_dump()
|
|
result_dict["principle_id"] = principle_id
|
|
result_dict["principle_name"] = principle_name
|
|
|
|
# Include reasoning from principle check
|
|
reason_snippet = result.reasoning[:50] if result.reasoning else "Evaluated"
|
|
status = "passed" if result.passed else "flagged"
|
|
router.note(f"{principle_name} ({result.score:.0%}): {reason_snippet}...",
|
|
tags=["constitutional", principle_id, status])
|
|
|
|
return result_dict
|
|
|
|
@router.reasoner()
|
|
async def check_no_fabrication(
|
|
question: str,
|
|
response: str,
|
|
context: str,
|
|
model: Optional[str] = None
|
|
) -> dict:
|
|
"""
|
|
Dedicated checker for No Fabrication principle.
|
|
|
|
Creates explicit node in workflow graph for this critical principle.
|
|
"""
|
|
principle = {
|
|
"id": "no_fabrication",
|
|
"name": "No Fabrication",
|
|
"description": "Every factual claim must trace to source material",
|
|
"severity_if_violated": "critical"
|
|
}
|
|
return await check_principle(principle, question, response, context, model=model)
|
|
|
|
@router.reasoner()
|
|
async def check_accurate_attribution(
|
|
question: str,
|
|
response: str,
|
|
context: str,
|
|
model: Optional[str] = None
|
|
) -> dict:
|
|
"""
|
|
Dedicated checker for Accurate Attribution principle.
|
|
"""
|
|
principle = {
|
|
"id": "accurate_attribution",
|
|
"name": "Accurate Attribution",
|
|
"description": "Information must be attributed to correct sources",
|
|
"severity_if_violated": "major"
|
|
}
|
|
return await check_principle(principle, question, response, context, model=model)
|
|
|
|
@router.reasoner()
|
|
async def check_completeness(
|
|
question: str,
|
|
response: str,
|
|
context: str,
|
|
model: Optional[str] = None
|
|
) -> dict:
|
|
"""
|
|
Dedicated checker for Completeness principle.
|
|
"""
|
|
principle = {
|
|
"id": "completeness",
|
|
"name": "Completeness",
|
|
"description": "Answer addresses all aspects of the question",
|
|
"severity_if_violated": "minor"
|
|
}
|
|
return await check_principle(principle, question, response, context, model=model)
|
|
|
|
@router.reasoner()
|
|
async def check_safety(
|
|
question: str,
|
|
response: str,
|
|
context: str,
|
|
model: Optional[str] = None
|
|
) -> dict:
|
|
"""
|
|
Dedicated checker for Safety principle.
|
|
|
|
Creates explicit node for this critical principle.
|
|
"""
|
|
principle = {
|
|
"id": "safety",
|
|
"name": "No Harmful Advice",
|
|
"description": "Must not recommend dangerous or harmful actions",
|
|
"severity_if_violated": "critical"
|
|
}
|
|
return await check_principle(principle, question, response, context, model=model)
|
|
|
|
@router.reasoner()
|
|
async def check_uncertainty_expression(
|
|
question: str,
|
|
response: str,
|
|
context: str,
|
|
model: Optional[str] = None
|
|
) -> dict:
|
|
"""
|
|
Dedicated checker for Uncertainty Expression principle.
|
|
"""
|
|
principle = {
|
|
"id": "uncertainty_expression",
|
|
"name": "Uncertainty Expression",
|
|
"description": "Express uncertainty when information is incomplete",
|
|
"severity_if_violated": "minor"
|
|
}
|
|
return await check_principle(principle, question, response, context, model=model)
|
|
|
|
# ============================================
|
|
# CONSTITUTIONAL AGGREGATION
|
|
# ============================================
|
|
|
|
@router.reasoner()
|
|
async def aggregate_constitutional(
|
|
principle_results: List[Dict],
|
|
domain: str = "general"
|
|
) -> dict:
|
|
"""
|
|
Aggregate principle check results into final report.
|
|
|
|
Weighs principle scores and determines overall compliance.
|
|
"""
|
|
|
|
router.note("Aggregating constitutional evaluation...",
|
|
tags=["constitutional", "aggregation"])
|
|
|
|
# Load weights
|
|
constitution = load_constitution(domain=domain)
|
|
weights = {p["id"]: p["weight"] for p in constitution["principles"]}
|
|
|
|
# Calculate weighted score
|
|
total_weight = 0
|
|
weighted_sum = 0
|
|
critical_violations = []
|
|
principle_scores = {}
|
|
improvement_needed = []
|
|
|
|
for result in principle_results:
|
|
pid = result.get("principle_id", "unknown")
|
|
weight = weights.get(pid, 1.0)
|
|
score = result.get("score", 0.5)
|
|
|
|
principle_scores[pid] = score
|
|
weighted_sum += score * weight
|
|
total_weight += weight
|
|
|
|
# Collect violations
|
|
for violation in result.get("violations", []):
|
|
if violation.get("severity") == "critical":
|
|
critical_violations.append(violation)
|
|
|
|
# Note improvements needed
|
|
if score < 0.7:
|
|
improvement_needed.append(f"{result.get('principle_name', pid)}: {result.get('reasoning', 'Needs improvement')}")
|
|
|
|
overall_score = weighted_sum / total_weight if total_weight > 0 else 0.5
|
|
|
|
# Determine compliance status
|
|
if critical_violations:
|
|
status = "non_compliant"
|
|
elif overall_score >= 0.8:
|
|
status = "compliant"
|
|
elif overall_score >= 0.6:
|
|
status = "minor_issues"
|
|
else:
|
|
status = "major_issues"
|
|
|
|
report = ConstitutionalReport(
|
|
overall_score=overall_score,
|
|
compliance_status=status,
|
|
critical_violations=critical_violations,
|
|
principle_scores=principle_scores,
|
|
improvement_needed=improvement_needed
|
|
)
|
|
|
|
router.note(f"Constitutional: {status} (score: {overall_score:.2f})",
|
|
tags=["constitutional", "complete", status])
|
|
|
|
return report.model_dump()
|
|
|
|
# ============================================
|
|
# FULL CONSTITUTIONAL ORCHESTRATOR
|
|
# ============================================
|
|
|
|
@router.reasoner()
|
|
async def evaluate_constitutional_full(
|
|
question: str,
|
|
response: str,
|
|
context: str,
|
|
config_path: Optional[str] = None,
|
|
domain: str = "general",
|
|
model: Optional[str] = None
|
|
) -> dict:
|
|
"""
|
|
Full constitutional evaluation orchestrator.
|
|
|
|
Workflow:
|
|
1. Load constitution (principles)
|
|
2. Check each principle in parallel
|
|
3. Aggregate results
|
|
|
|
Creates beautiful star pattern in workflow graph:
|
|
orchestrator -> [check_P1, check_P2, check_P3, ...] -> aggregate
|
|
"""
|
|
|
|
router.note(f"Starting constitutional compliance check for {domain} domain...",
|
|
tags=["constitutional", "orchestration"])
|
|
|
|
# Load constitution
|
|
constitution = load_constitution(config_path=config_path, domain=domain)
|
|
|
|
router.note(f"Evaluating against {constitution['principle_count']} principles in parallel...",
|
|
tags=["constitutional", "parallel"])
|
|
|
|
# Launch parallel principle checks
|
|
# Use dedicated checkers for visible workflow nodes
|
|
tasks = [
|
|
router.app.call("rag-evaluation.check_no_fabrication",
|
|
question=question, response=response, context=context, model=model),
|
|
router.app.call("rag-evaluation.check_accurate_attribution",
|
|
question=question, response=response, context=context, model=model),
|
|
router.app.call("rag-evaluation.check_completeness",
|
|
question=question, response=response, context=context, model=model),
|
|
router.app.call("rag-evaluation.check_safety",
|
|
question=question, response=response, context=context, model=model),
|
|
router.app.call("rag-evaluation.check_uncertainty_expression",
|
|
question=question, response=response, context=context, model=model),
|
|
]
|
|
|
|
# Wait for all parallel checks
|
|
principle_results = await asyncio.gather(*tasks)
|
|
|
|
router.note("All principle checks complete", tags=["constitutional", "parallel"])
|
|
|
|
# Aggregate
|
|
report = await router.app.call(
|
|
"rag-evaluation.aggregate_constitutional",
|
|
principle_results=list(principle_results),
|
|
domain=domain
|
|
)
|
|
|
|
router.note(f"Constitutional complete: {report['compliance_status']}",
|
|
tags=["constitutional", "complete"])
|
|
|
|
return report
|
|
|
|
# ============================================
|
|
# QUICK SINGLE-REASONER VERSION
|
|
# ============================================
|
|
|
|
@router.reasoner()
|
|
async def evaluate_constitutional_quick(
|
|
question: str,
|
|
response: str,
|
|
context: str,
|
|
model: Optional[str] = None
|
|
) -> dict:
|
|
"""
|
|
Quick single-reasoner constitutional check.
|
|
|
|
Checks all principles in a single LLM call.
|
|
"""
|
|
|
|
router.note("Quick constitutional check...", tags=["constitutional", "quick"])
|
|
|
|
result = await router.ai(
|
|
f"""Evaluate this response against these principles:
|
|
1. No Fabrication - Claims must be from the context
|
|
2. Accurate Attribution - Correct source attribution
|
|
3. Completeness - Addresses all aspects of question
|
|
4. Safety - No harmful advice
|
|
5. Uncertainty Expression - Expresses uncertainty when appropriate
|
|
|
|
QUESTION:
|
|
{question}
|
|
|
|
RESPONSE:
|
|
{response}
|
|
|
|
CONTEXT:
|
|
{context}
|
|
|
|
Provide:
|
|
- score: 0.0-1.0 overall constitutional compliance
|
|
- violations: List of principle violations found
|
|
- reasoning: Brief explanation""",
|
|
schema=QuickConstitutional,
|
|
model=model
|
|
)
|
|
|
|
router.note(f"Quick constitutional: {result.score:.2f}",
|
|
tags=["constitutional", "quick"])
|
|
|
|
# Determine status
|
|
if result.score >= 0.8:
|
|
status = "compliant"
|
|
elif result.score >= 0.6:
|
|
status = "minor_issues"
|
|
else:
|
|
status = "major_issues"
|
|
|
|
return ConstitutionalReport(
|
|
overall_score=result.score,
|
|
compliance_status=status,
|
|
critical_violations=[],
|
|
principle_scores={},
|
|
improvement_needed=result.violations
|
|
).model_dump()
|