diff --git a/opus_orchestrator/agents/base.py b/opus_orchestrator/agents/base.py index b456a47..e44dcfc 100644 --- a/opus_orchestrator/agents/base.py +++ b/opus_orchestrator/agents/base.py @@ -93,6 +93,49 @@ class BaseAgent(ABC, Generic[T]): temperature=temp, max_tokens=self.config.max_tokens, ) + def validate_output(self, output: Any, schema: type[T]) -> tuple[bool, Optional[T], Optional[str]]: + """Validate agent output against a Pydantic schema. + + Args: + output: Raw output from the agent (usually str) + schema: Pydantic model to validate against + + Returns: + Tuple of (is_valid, validated_output, error_message) + """ + if schema is None: + # No schema to validate against + return True, None, None + + # Try to parse the output as the schema + try: + # If output is a string, try to parse as JSON first + if isinstance(output, str): + import json + # Try to extract JSON from the output + try: + # Look for JSON block + if "```json" in output: + json_str = output.split("```json")[1].split("```")[0] + elif "```" in output: + json_str = output.split("```")[1].split("```")[0] + else: + json_str = output + data = json.loads(json_str) + except (json.JSONDecodeError, IndexError): + # Not JSON, return as-is + return True, None, None + else: + data = output + + # Validate against schema + validated = schema(**data) + return True, validated, None + + except Exception as e: + return False, None, f"Validation failed: {str(e)}" + + def build_system_prompt(self, context: dict[str, Any]) -> str: """Build the full system prompt with context. diff --git a/opus_orchestrator/state_adapter.py b/opus_orchestrator/state_adapter.py new file mode 100644 index 0000000..8965d97 --- /dev/null +++ b/opus_orchestrator/state_adapter.py @@ -0,0 +1,134 @@ +"""Unified State Adapter for Opus Orchestrator. + +Provides adapters between OpusState and OpusGraphState to unify +the two state systems. +""" + +from typing import Any, Optional + +from opus_orchestrator.state import OpusState +from opus_orchestrator.langgraph_state import OpusGraphState + + +class StateAdapter: + """Adapter to convert between OpusState and OpusGraphState.""" + + @staticmethod + def opus_to_graph(opus_state: OpusState) -> dict[str, Any]: + """Convert OpusState to dict for OpusGraphState. + + Args: + opus_state: The OpusState to convert + + Returns: + Dict suitable for creating OpusGraphState + """ + # Extract pre-writing data if available + prewriting_data = {} + + if opus_state.blueprint: + prewriting_data = { + "one_sentence": opus_state.blueprint.title, # Approximate + "one_paragraph": opus_state.blueprint.subtitle or "", + } + + return { + "seed_concept": opus_state.raw_content.text[:500] if opus_state.raw_content else "", + "framework": "snowflake", + "genre": opus_state.intent.genre if opus_state.intent else "general", + "target_word_count": opus_state.intent.target_word_count if opus_state.intent else 80000, + "prewriting": prewriting_data, + } + + @staticmethod + def graph_to_opus(graph_state: OpusGraphState) -> OpusState: + """Convert OpusGraphState back to OpusState. + + This is lossy - only preserves key fields. + + Args: + graph_state: The OpusGraphState to convert + + Returns: + OpusState with available data + """ + from opus_orchestrator.schemas import BookIntent, BookType + + intent = BookIntent( + book_type=BookType.FICTION, + genre=graph_state.genre, + target_word_count=graph_state.target_word_count, + target_audience="general readers", + intended_outcome="complete novel", + ) + + opus_state = OpusState( + intent=intent, + current_stage=graph_state.stage.value, + progress=graph_state.progress, + ) + + # Add manuscript if available + if graph_state.manuscript: + from opus_orchestrator.schemas import Manuscript, Chapter + chapters = [] + for i, ch_state in graph_state.chapters.items(): + chapters.append(Chapter( + chapter_number=i, + title=ch_state.content[:50], + content=ch_state.content, + word_count=ch_state.word_count, + )) + + opus_state.manuscript = Manuscript( + title="Untitled", + book_type=BookType.FICTION, + genre=graph_state.genre, + chapters=chapters, + total_word_count=graph_state.total_word_count, + ) + + return opus_state + + @staticmethod + def create_unified_state( + seed_concept: str, + framework: str = "snowflake", + genre: str = "general", + target_word_count: int = 80000, + ) -> dict[str, Any]: + """Create initial state dict that works for both systems. + + Args: + seed_concept: The seed concept for the book + framework: Story framework to use + genre: Genre of the book + target_word_count: Target word count + + Returns: + Dict that can initialize either state model + """ + return { + # Common fields + "seed_concept": seed_concept, + "framework": framework, + "genre": genre, + "target_word_count": target_word_count, + # For OpusState compatibility + "repo_url": None, + "intent": None, + "raw_content": None, + # For OpusGraphState compatibility + "prewriting": {}, + "style_guide": "", + "current_chapter": 0, + "chapters": {}, + "manuscript": "", + "total_word_count": 0, + "use_autogen": True, + "critique_iterations": {}, + "validation_errors": [], + "warnings": [], + "progress": 0.0, + "messages": [], + }