Implement Phase 1: LangGraph backend MVP
Sets up the full backend foundation for CouncilOS:
- CouncilState TypedDict with all required fields and LangGraph reducers
- Three agent nodes: master_agent (drafts), critic_agent (scores + routes),
writer_agent (final polish)
- LangGraph graph with cyclic rework loop: Master → Critic → (score < 8:
back to Master | score ≥ 8: Writer → END)
- Safety valve: MAX_ITERATIONS=5 prevents infinite loops
- FastAPI app with REST endpoints (POST /api/councils/run, GET /api/councils/run/{id})
and WebSocket endpoint (/ws/council/{run_id}) for real-time agent status events
- In-memory RunStore for Phase 1 (PostgreSQL-backed in Phase 3)
- pytest test suite: state, routing logic, critic parser, agent nodes, API endpoints
- .env.example, .gitignore, docker-compose.yml, Dockerfile
https://claude.ai/code/session_01RfMpt3TbMjZEtK3CAyP5iQ
This commit is contained in:
parent
34dcfb3dcd
commit
797f02c74d
24 changed files with 1472 additions and 0 deletions
7
backend/agents/__init__.py
Normal file
7
backend/agents/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""Agent node functions for CouncilOS."""
|
||||
|
||||
from .master_agent import master_agent_node
|
||||
from .critic_agent import critic_agent_node
|
||||
from .writer_agent import writer_agent_node
|
||||
|
||||
__all__ = ["master_agent_node", "critic_agent_node", "writer_agent_node"]
|
||||
127
backend/agents/critic_agent.py
Normal file
127
backend/agents/critic_agent.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
"""
|
||||
Critic Agent Node — evaluates the current draft and decides whether to approve or rework.
|
||||
|
||||
The critic scores the draft from 0–10 and returns structured feedback.
|
||||
If the score meets APPROVAL_THRESHOLD, route_decision is set to "approve".
|
||||
Otherwise it is set to "rework" and the feedback is appended to feedback_history.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
|
||||
from state import CouncilState, APPROVAL_THRESHOLD, MAX_ITERATIONS
|
||||
|
||||
|
||||
_SYSTEM_PROMPT = """You are the Critic AI in a council of expert AIs.
|
||||
Your job is to rigorously evaluate the quality of a draft document.
|
||||
|
||||
You must respond in EXACTLY this format — no deviations:
|
||||
|
||||
SCORE: <integer 0-10>
|
||||
VERDICT: <"approve" if score >= 8, otherwise "rework">
|
||||
FEEDBACK:
|
||||
<detailed, actionable feedback explaining what must be improved>
|
||||
|
||||
Scoring criteria:
|
||||
- 0–3: Poor structure, major factual gaps, incoherent
|
||||
- 4–6: Adequate but needs significant improvement
|
||||
- 7: Good but has notable weaknesses
|
||||
- 8–9: High quality, minor improvements possible
|
||||
- 10: Exceptional, publication-ready
|
||||
|
||||
Be strict. Only award 8+ if the document genuinely meets high quality standards."""
|
||||
|
||||
|
||||
def _parse_critic_response(content: str) -> tuple[float, str, str]:
|
||||
"""
|
||||
Parse the structured critic response.
|
||||
|
||||
Returns:
|
||||
(score, verdict, feedback) tuple.
|
||||
Falls back to ("rework", full content) on parse failure.
|
||||
"""
|
||||
score_match = re.search(r"SCORE:\s*(\d+(?:\.\d+)?)", content)
|
||||
verdict_match = re.search(r"VERDICT:\s*(approve|rework)", content, re.IGNORECASE)
|
||||
feedback_match = re.search(r"FEEDBACK:\s*(.*)", content, re.DOTALL)
|
||||
|
||||
score = float(score_match.group(1)) if score_match else 0.0
|
||||
verdict = verdict_match.group(1).lower() if verdict_match else "rework"
|
||||
feedback = feedback_match.group(1).strip() if feedback_match else content.strip()
|
||||
|
||||
# Clamp score to 0–10
|
||||
score = max(0.0, min(10.0, score))
|
||||
|
||||
return score, verdict, feedback
|
||||
|
||||
|
||||
def critic_agent_node(state: CouncilState) -> dict:
|
||||
"""
|
||||
LangGraph node function for the Critic Agent.
|
||||
|
||||
Reads current_draft from state, evaluates it, and returns:
|
||||
- route_decision: "approve" or "rework"
|
||||
- critic_score: numeric score
|
||||
- feedback_history: appended with new feedback (if rework)
|
||||
- active_node: "critic_agent"
|
||||
|
||||
Safety valve: if iteration_count >= MAX_ITERATIONS, force approval
|
||||
to prevent infinite loops.
|
||||
|
||||
Args:
|
||||
state: The current CouncilState.
|
||||
|
||||
Returns:
|
||||
A dict with updated state fields.
|
||||
"""
|
||||
# Safety valve: prevent infinite loops
|
||||
if state.get("iteration_count", 0) >= MAX_ITERATIONS:
|
||||
return {
|
||||
"route_decision": "approve",
|
||||
"critic_score": APPROVAL_THRESHOLD,
|
||||
"feedback_history": [
|
||||
f"[Auto-approved after {MAX_ITERATIONS} iterations]"
|
||||
],
|
||||
"messages": [],
|
||||
"active_node": "critic_agent",
|
||||
}
|
||||
|
||||
llm = ChatAnthropic(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
api_key=os.environ.get("ANTHROPIC_API_KEY"),
|
||||
temperature=0.2, # Low temperature for consistent evaluation
|
||||
max_tokens=1024,
|
||||
)
|
||||
|
||||
system_msg = SystemMessage(content=_SYSTEM_PROMPT)
|
||||
user_msg = HumanMessage(
|
||||
content=(
|
||||
f"Please evaluate this draft on the topic '{state['input_topic']}':\n\n"
|
||||
f"{state['current_draft']}"
|
||||
)
|
||||
)
|
||||
|
||||
response = llm.invoke([system_msg, user_msg])
|
||||
score, verdict, feedback = _parse_critic_response(response.content)
|
||||
|
||||
# Override verdict based on threshold to ensure consistency
|
||||
if score >= APPROVAL_THRESHOLD:
|
||||
route_decision = "approve"
|
||||
else:
|
||||
route_decision = "rework"
|
||||
|
||||
result: dict = {
|
||||
"critic_score": score,
|
||||
"route_decision": route_decision,
|
||||
"messages": [system_msg, user_msg, response],
|
||||
"active_node": "critic_agent",
|
||||
}
|
||||
|
||||
# Only append feedback if we're sending back for rework
|
||||
if route_decision == "rework":
|
||||
result["feedback_history"] = [
|
||||
f"Score: {score}/10\n{feedback}"
|
||||
]
|
||||
|
||||
return result
|
||||
75
backend/agents/master_agent.py
Normal file
75
backend/agents/master_agent.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
"""
|
||||
Master Agent Node — creates and refines drafts based on critic feedback.
|
||||
|
||||
This agent is the primary content creator. On the first iteration it produces
|
||||
an initial draft. On subsequent iterations it incorporates all feedback from
|
||||
the feedback_history to improve the draft.
|
||||
"""
|
||||
|
||||
import os
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
|
||||
from state import CouncilState
|
||||
|
||||
|
||||
_SYSTEM_PROMPT = """You are the Master AI in a council of expert AIs.
|
||||
Your job is to write high-quality content on the given topic.
|
||||
When you receive critic feedback, carefully incorporate ALL feedback points
|
||||
and produce an improved draft. Be thorough and precise."""
|
||||
|
||||
|
||||
def _build_master_prompt(state: CouncilState) -> str:
|
||||
"""Build the user-facing prompt for the master agent based on current state."""
|
||||
if not state["feedback_history"]:
|
||||
return (
|
||||
f"Please write a comprehensive, well-structured document on the following topic:\n\n"
|
||||
f"{state['input_topic']}"
|
||||
)
|
||||
|
||||
feedback_block = "\n\n---\n".join(
|
||||
f"Feedback round {i + 1}:\n{fb}"
|
||||
for i, fb in enumerate(state["feedback_history"])
|
||||
)
|
||||
|
||||
return (
|
||||
f"Topic: {state['input_topic']}\n\n"
|
||||
f"Your current draft:\n{state['current_draft']}\n\n"
|
||||
f"The critic has provided the following feedback across {len(state['feedback_history'])} round(s):\n\n"
|
||||
f"{feedback_block}\n\n"
|
||||
f"Please produce an improved draft that fully addresses ALL feedback points above."
|
||||
)
|
||||
|
||||
|
||||
def master_agent_node(state: CouncilState) -> dict:
|
||||
"""
|
||||
LangGraph node function for the Master Agent.
|
||||
|
||||
Reads input_topic and feedback_history from state, calls the LLM,
|
||||
and returns an updated current_draft.
|
||||
|
||||
Args:
|
||||
state: The current CouncilState.
|
||||
|
||||
Returns:
|
||||
A dict with updated state fields: current_draft, messages, active_node.
|
||||
"""
|
||||
llm = ChatAnthropic(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
api_key=os.environ.get("ANTHROPIC_API_KEY"),
|
||||
temperature=0.7,
|
||||
max_tokens=2048,
|
||||
)
|
||||
|
||||
system_msg = SystemMessage(content=_SYSTEM_PROMPT)
|
||||
user_msg = HumanMessage(content=_build_master_prompt(state))
|
||||
|
||||
response = llm.invoke([system_msg, user_msg])
|
||||
draft = response.content
|
||||
|
||||
return {
|
||||
"current_draft": draft,
|
||||
"messages": [system_msg, user_msg, response],
|
||||
"active_node": "master_agent",
|
||||
"iteration_count": state.get("iteration_count", 0) + 1,
|
||||
}
|
||||
63
backend/agents/writer_agent.py
Normal file
63
backend/agents/writer_agent.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
"""
|
||||
Writer Agent Node — final polishing of an approved draft.
|
||||
|
||||
This agent receives a critic-approved draft and produces the final,
|
||||
publication-ready version with polished formatting and language.
|
||||
"""
|
||||
|
||||
import os
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
|
||||
from state import CouncilState
|
||||
|
||||
|
||||
_SYSTEM_PROMPT = """You are the Writer AI in a council of expert AIs.
|
||||
You receive a draft that has already been approved for quality by the Critic AI.
|
||||
Your job is to give it a final professional polish:
|
||||
|
||||
- Improve sentence flow and readability
|
||||
- Ensure consistent formatting (headers, bullet points, paragraphs)
|
||||
- Fix any grammatical or stylistic issues
|
||||
- Do NOT change the factual content or overall structure
|
||||
- Preserve all key information from the draft
|
||||
|
||||
Return only the polished document — no meta-commentary."""
|
||||
|
||||
|
||||
def writer_agent_node(state: CouncilState) -> dict:
|
||||
"""
|
||||
LangGraph node function for the Writer Agent.
|
||||
|
||||
Receives the approved current_draft and returns a polished final version.
|
||||
|
||||
Args:
|
||||
state: The current CouncilState.
|
||||
|
||||
Returns:
|
||||
A dict with the final polished current_draft and updated messages.
|
||||
"""
|
||||
llm = ChatAnthropic(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
api_key=os.environ.get("ANTHROPIC_API_KEY"),
|
||||
temperature=0.4,
|
||||
max_tokens=4096,
|
||||
)
|
||||
|
||||
system_msg = SystemMessage(content=_SYSTEM_PROMPT)
|
||||
user_msg = HumanMessage(
|
||||
content=(
|
||||
f"Please give a final professional polish to this approved document "
|
||||
f"on the topic '{state['input_topic']}':\n\n"
|
||||
f"{state['current_draft']}"
|
||||
)
|
||||
)
|
||||
|
||||
response = llm.invoke([system_msg, user_msg])
|
||||
|
||||
return {
|
||||
"current_draft": response.content,
|
||||
"messages": [system_msg, user_msg, response],
|
||||
"active_node": "writer_agent",
|
||||
"route_decision": "done",
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue