VentureForge / src /graph.py
Raiquia's picture
improve codebase
b9f4b13
Raw History Blame Contribute Delete
5.38 kB
"""
VentureForge LangGraph
======================
Assembles the hierarchical multi-agent graph with reflection loop and SQLite checkpoint persistence.
Usage:
from src.graph import build_graph, GRAPH
graph = build_graph()
result = graph.invoke(state)
"""
from __future__ import annotations
import logging
import sqlite3
import time
from collections.abc import Callable
from pathlib import Path
from typing import Any
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.checkpoint.memory import MemorySaver
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import END, StateGraph
from src.agents import critic as critic_agent
from src.agents import idea_generator as idea_generator_agent
from src.agents import pain_point_miner as pain_point_miner_agent
from src.agents import pitch_writer as pitch_writer_agent
from src.agents import scorer as scorer_agent
from src.agents.orchestrator import orchestrator
from src.state.graph_state import VentureForgeState
logger = logging.getLogger(__name__)
DEFAULT_CHECKPOINT_DB_PATH = ".cache/ventureforge.db"
ALLOWED_MSGPACK_MODULES = [
("src.models.common", "PipelineStage"),
("src.models.common", "DataSource"),
("src.models.common", "Verdict"),
("src.models.common", "TargetAgent"),
("src.models.common", "RunEvent"),
("src.models.common", "ErrorEntry"),
("src.models.pain_point", "PainPoint"),
("src.models.pain_point", "PainPointEvidence"),
("src.models.pain_point", "PainPointRubric"),
("src.models.idea", "Idea"),
("src.models.idea", "ScoredIdea"),
("src.models.pitch", "PitchBrief"),
("src.models.critique", "Critique"),
("src.models.critique", "CritiqueRubric"),
]
def timed(
name: str,
fn: Callable[[VentureForgeState], dict[str, Any]],
) -> Callable[[VentureForgeState], dict[str, Any]]:
"""Wrap a worker node function with elapsed-time recording.
Returns a closure that calls fn(state), merges the agent_timings
patch, and preserves the original function name for LangGraph.
"""
def _wrapper(state: VentureForgeState) -> dict[str, Any]:
t0 = time.monotonic()
result = fn(state)
return {**result, **state.record_timing(name, time.monotonic() - t0)}
_wrapper.__name__ = name
_wrapper.__qualname__ = name
return _wrapper
def get_checkpointer(db_path: str | None = DEFAULT_CHECKPOINT_DB_PATH) -> BaseCheckpointSaver:
"""Create a persistent SQLite checkpointer, falling back to in-memory if unavailable."""
serde = JsonPlusSerializer().with_msgpack_allowlist(ALLOWED_MSGPACK_MODULES)
if db_path:
try:
db_file = Path(db_path)
db_file.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(db_file), check_same_thread=False)
logger.info(f"[graph] Initialized SqliteSaver checkpoint persistence at '{db_path}'.")
return SqliteSaver(conn, serde=serde)
except Exception as e:
logger.warning(
f"[graph] Failed to initialize SQLite checkpointer at '{db_path}': {e}. Using MemorySaver."
)
return MemorySaver(serde=serde)
def route_after_orchestrator(state: VentureForgeState) -> str:
"""Return the next node name after the orchestrator runs."""
return state.next_node
def route_after_critic(state: VentureForgeState) -> str:
"""After critic, always return to orchestrator for routing decisions."""
return "orchestrator"
def build_graph(checkpointer: BaseCheckpointSaver | None = None) -> StateGraph:
"""Build and return the compiled LangGraph StateGraph."""
workflow = StateGraph(VentureForgeState)
# Register nodes — workers are wrapped with timed() at the wiring site
workflow.add_node("orchestrator", orchestrator)
workflow.add_node("pain_point_miner", timed("pain_point_miner", pain_point_miner_agent.run))
workflow.add_node("idea_generator", timed("idea_generator", idea_generator_agent.run))
workflow.add_node("scorer", timed("scorer", scorer_agent.run))
workflow.add_node("pitch_writer", timed("pitch_writer", pitch_writer_agent.run))
workflow.add_node("critic", timed("critic", critic_agent.run))
# Entry point
workflow.set_entry_point("orchestrator")
# Orchestrator routes to the appropriate worker (or end)
workflow.add_conditional_edges(
"orchestrator",
route_after_orchestrator,
{
"pain_point_miner": "pain_point_miner",
"idea_generator": "idea_generator",
"scorer": "scorer",
"pitch_writer": "pitch_writer",
"critic": "critic",
"__end__": END,
},
)
# Workers always return to orchestrator
for worker in ("pain_point_miner", "idea_generator", "scorer", "pitch_writer"):
workflow.add_edge(worker, "orchestrator")
# Critic returns to orchestrator
workflow.add_conditional_edges(
"critic",
route_after_critic,
{
"orchestrator": "orchestrator",
END: END,
},
)
saver = checkpointer if checkpointer is not None else get_checkpointer()
return workflow.compile(checkpointer=saver)
# Convenience: pre-compiled graph instance
GRAPH = build_graph()