Wire up all agents with LLM calls

- Worldsmith, Character Lead, Voice, Editor agents now call LLM
- All nonfiction agents wired (Researcher, Analyst, Writer, FactChecker, Editor)
- Orchestrator fully wired with agent pipeline
- Add python-dotenv dependency
This commit is contained in:
2026-03-12 18:42:15 +00:00
parent e151cee69f
commit dec5aae09a
7 changed files with 584 additions and 462 deletions
@@ -46,7 +46,7 @@ You are The Character Lead — the one who breathes life into the figures who in
- **Positive**: Growth from weakness - **Positive**: Growth from weakness
- **Negative**: Fall from grace - **Negative**: Fall from grace
- **Flat**: No change, changes world - **Flat**: No change, changes world
- **Disruption**: External力量打破平衡 - **Disruption**: External forces break equilibrium
## The Want/Need/Fear Triad ## The Want/Need/Fear Triad
@@ -77,39 +77,52 @@ class CharacterLeadAgent(BaseAgent):
) )
async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse: async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse:
"""Execute the Character Lead's task to generate character profiles. """Execute the Character Lead's task to generate character profiles."""
Args:
input_data: Raw content + blueprint with character references
context: Additional context
Returns:
AgentResponse with character profiles
"""
characters = input_data.get("characters", []) characters = input_data.get("characters", [])
raw_content = input_data.get("raw_content", "") raw_content = input_data.get("raw_content", "")
blueprint = input_data.get("blueprint", {})
user_prompt = f"""## Task user_prompt = f"""## Task
Create comprehensive character profiles for the following characters: Create comprehensive character profiles for the following story:
{chr(10).join(f"- {c}" for c in characters) if characters else "Create profiles for all characters in the story."} - Title: {blueprint.get('title', 'Untitled')}
- Genre: {blueprint.get('genre', 'general')}
{chr(10).join(f'- {c}' for c in characters) if characters else 'Create compelling characters that would drive this story.'}
## Raw Content Reference ## Raw Content Reference
{raw_content} {raw_content if raw_content else 'Create original characters appropriate for this genre and story.'}
## Guidelines ## Guidelines
Follow the Character Lead methodology from your system prompt. Follow the Character Lead methodology from your system prompt.
Include the Want/Need/Fear triad for each major character. Include the Want/Need/Fear triad for each major character.
Ensure each character has a distinct voice and arc.
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={"status": "characters_created"}, system_prompt=self.build_system_prompt(context),
metadata={"role": "Character Lead", "character_count": len(characters)}, user_prompt=user_prompt,
) )
return AgentResponse(
success=True,
output=result,
metadata={
"role": "Character Lead",
"character_count": len(characters) if characters else 0,
},
)
except Exception as e:
return AgentResponse(
success=False,
output=None,
error=str(e),
metadata={"role": "Character Lead"},
)
async def develop_relationship( async def develop_relationship(
self, self,
@@ -129,14 +142,27 @@ Include the Want/Need/Fear triad for each major character.
Develop this relationship following the Character Lead methodology. Develop this relationship following the Character Lead methodology.
Include: Include:
- Current dynamics - Current dynamics and power balance
- Power balance
- History (if any) - History (if any)
- Potential arc - Potential arc throughout the story
- Key moments that define the relationship
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={"status": "relationship_developed"}, system_prompt=self.build_system_prompt(context),
metadata={"role": "Character Lead", "characters": [character_a, character_b]}, user_prompt=user_prompt,
) )
return AgentResponse(
success=True,
output=result,
metadata={"role": "Character Lead", "characters": [character_a, character_b]},
)
except Exception as e:
return AgentResponse(
success=False,
output=None,
error=str(e),
metadata={"role": "Character Lead"},
)
+103 -49
View File
@@ -64,12 +64,14 @@ You are The Editor — the quality control mechanism, identifying problems acros
- **Minor Revisions**: Continuity errors, style inconsistencies, pacing tweaks - **Minor Revisions**: Continuity errors, style inconsistencies, pacing tweaks
- **Polish**: Grammar, punctuation, word choice refinement - **Polish**: Grammar, punctuation, word choice refinement
## Quality Standards ## Output Format
- Every issue must have specific, actionable feedback Provide your critique as a structured review with:
- Revision priorities must be clearly ordered 1. Overall score (0.0-1.0)
- Continuity issues must be flagged with exact locations 2. Strengths (list)
- Pacing analysis must be data-driven (scene lengths, tension scores) 3. Weaknesses (list)
4. Specific revision suggestions (prioritized)
5. Final verdict: major_revisions / minor_revisions / approved
""" """
@@ -85,41 +87,43 @@ class EditorAgent(BaseAgent):
) )
async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse: async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse:
"""Execute the Editor's task to review and assess the manuscript. """Execute the Editor's task to review content."""
Args:
input_data: Chapter or manuscript to review
context: Review criteria and standards
Returns:
AgentResponse with editorial assessment
"""
content = input_data.get("content", "") content = input_data.get("content", "")
review_type = input_data.get("review_type", "full") review_type = input_data.get("review_type", "full")
user_prompt = f"""## Task user_prompt = f"""## Task
Perform a {review_type} editorial review on: Perform a {review_type} editorial review on the following content:
{content[:5000]}... {'(truncated)' if len(content) > 5000 else ''} {content}
## Review Type: {review_type} ## Review Type: {review_type}
## Guidelines ## Guidelines
Follow the Editor methodology from your system prompt. Follow the Editor methodology from your system prompt.
Include: Be specific and actionable in your feedback.
- Continuity verification Assign a clear revision priority.
- Pacing analysis
- Quality assessment
- Specific revision directions
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={"status": "editorial_review_complete"}, system_prompt=self.build_system_prompt(context),
metadata={"role": "Editor", "review_type": review_type}, user_prompt=user_prompt,
) )
return AgentResponse(
success=True,
output=result,
metadata={"role": "Editor", "review_type": review_type},
)
except Exception as e:
return AgentResponse(
success=False,
output=None,
error=str(e),
metadata={"role": "Editor"},
)
async def review_chapter( async def review_chapter(
self, self,
@@ -132,14 +136,15 @@ Include:
- Chapter Number: {chapter.get('chapter_number')} - Chapter Number: {chapter.get('chapter_number')}
- Title: {chapter.get('title')} - Title: {chapter.get('title')}
- Content: {chapter.get('content', '')[:3000]}... - Content:
{chapter.get('content', '')}
## Full Manuscript Context ## Full Manuscript Context
- Total Chapters: {full_manuscript_context.get('total_chapters', 0)} - Total Chapters: {full_manuscript_context.get('total_chapters', 0)}
- Previous Chapters Summary: {full_manuscript_context.get('previous_summaries', [])} - Book Title: {full_manuscript_context.get('title', 'Untitled')}
- Characters in Story: {', '.join(full_manuscript_context.get('characters', []))} - Genre: {full_manuscript_context.get('genre', 'general')}
- World Rules: {full_manuscript_context.get('world_rules', {})}
## Task ## Task
@@ -150,18 +155,52 @@ Perform a complete editorial review of this chapter, considering:
- World-rule adherence - World-rule adherence
- Voice consistency - Voice consistency
- Dialogue quality - Dialogue quality
- Show vs. tell balance
Assign a revision priority: major_revisions, minor_revisions, or approved Provide:
1. Overall score (0.0-1.0)
2. Strengths (at least 3)
3. Weaknesses (at least 3)
4. Specific revision suggestions
5. Final verdict: major_revisions, minor_revisions, or approved
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={ system_prompt=self.build_system_prompt(context),
"status": "chapter_reviewed", user_prompt=user_prompt,
"chapter_number": chapter.get("chapter_number"), )
},
metadata={"role": "Editor", "task": "chapter_review"}, # Try to extract score from result
) score = 0.5 # default
for line in result.split('\n'):
if 'score' in line.lower() or 'rating' in line.lower():
try:
# Look for number
import re
numbers = re.findall(r'0\.\d+|\d+\.\d+', line)
if numbers:
score = float(numbers[0])
break
except:
pass
return AgentResponse(
success=True,
output={
"critique": result,
"score": score,
"chapter_number": chapter.get("chapter_number"),
},
metadata={"role": "Editor", "task": "chapter_review"},
)
except Exception as e:
return AgentResponse(
success=False,
output=None,
error=str(e),
metadata={"role": "Editor"},
)
async def generate_revision_notes( async def generate_revision_notes(
self, self,
@@ -169,23 +208,38 @@ Assign a revision priority: major_revisions, minor_revisions, or approved
context: dict[str, Any], context: dict[str, Any],
) -> AgentResponse: ) -> AgentResponse:
"""Generate prioritized revision notes from multiple critiques.""" """Generate prioritized revision notes from multiple critiques."""
critiques_text = "\n\n".join(f"### Critique {i+1}:\n{c.get('critique', str(c))}" for i, c in enumerate(critiques))
user_prompt = f"""## Critiques to Synthesize user_prompt = f"""## Critiques to Synthesize
{chr(10).join(f"### Critique {i+1}:{c}" for i, c in enumerate(critiques))} {critiques_text}
## Task ## Task
Synthesize these critiques into prioritized revision notes. Synthesize these critiques into prioritized revision notes.
Group by: Group by:
1. Major revisions (structural, plot, arc issues) 1. Major revisions (structural, plot, arc issues) - must fix
2. Minor revisions (continuity, style, pacing) 2. Minor revisions (continuity, style, pacing) - should fix
3. Polish items (grammar, word choice) 3. Polish items (grammar, word choice) - nice to fix
For each item, provide specific, actionable feedback. For each item, provide specific, actionable feedback with location if possible.
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={"status": "revision_notes_generated"}, system_prompt=self.build_system_prompt(context),
metadata={"role": "Editor", "critique_count": len(critiques)}, user_prompt=user_prompt,
) )
return AgentResponse(
success=True,
output=result,
metadata={"role": "Editor", "critique_count": len(critiques)},
)
except Exception as e:
return AgentResponse(
success=False,
output=None,
error=str(e),
metadata={"role": "Editor"},
)
+83 -42
View File
@@ -78,17 +78,10 @@ class VoiceAgent(BaseAgent):
) )
async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse: async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse:
"""Execute the Voice agent's task to create style guide and samples. """Execute the Voice agent's task to create style guide and samples."""
Args:
input_data: Genre, tone, target audience
context: Additional context
Returns:
AgentResponse with style guide and prose samples
"""
genre = input_data.get("genre", "general") genre = input_data.get("genre", "general")
tone = input_data.get("tone", "neutral") tone = input_data.get("tone", "neutral")
target_audience = input_data.get("target_audience", "General readers")
user_prompt = f"""## Task user_prompt = f"""## Task
@@ -96,41 +89,56 @@ Create a voice/style guide and prose samples for:
- Genre: {genre} - Genre: {genre}
- Tone: {tone} - Tone: {tone}
- Target Audience: {input_data.get('target_audience', 'General readers')} - Target Audience: {target_audience}
## Guidelines ## Guidelines
Follow the Voice agent methodology from your system prompt. Follow the Voice agent methodology from your system prompt.
Include: Include:
- Word bank - Word bank (preferred vocabulary for this genre/tone)
- Phrase patterns - Phrase patterns (recurring constructions)
- Rhythm map - Rhythm map (sentence length distribution)
- Tone guide - Tone guide (emotional range)
- 3 sample scenes (opening, dialogue, descriptive) - 3 sample scenes:
1. Opening scene
2. Dialogue-heavy scene
3. Descriptive/pacific scene
Make the samples vivid and representative of the final prose style.
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={"status": "voice_created"}, system_prompt=self.build_system_prompt(context),
metadata={"role": "Voice", "genre": genre, "tone": tone}, user_prompt=user_prompt,
) )
return AgentResponse(
success=True,
output=result,
metadata={"role": "Voice", "genre": genre, "tone": tone},
)
except Exception as e:
return AgentResponse(
success=False,
output=None,
error=str(e),
metadata={"role": "Voice"},
)
async def write_chapter( async def write_chapter(
self, self,
chapter_spec: dict[str, Any], chapter_spec: dict[str, Any],
style_guide: dict[str, Any], style_guide: str,
context: dict[str, Any], context: dict[str, Any],
) -> AgentResponse: ) -> AgentResponse:
"""Write a complete chapter following the style guide. """Write a complete chapter following the style guide."""
This is the main writing task for the Voice agent.
"""
user_prompt = f"""## Chapter Specification user_prompt = f"""## Chapter Specification
- Chapter Number: {chapter_spec.get('chapter_number')} - Chapter Number: {chapter_spec.get('chapter_number')}
- Title: {chapter_spec.get('title')} - Title: {chapter_spec.get('title')}
- Summary: {chapter_spec.get('summary')} - Summary: {chapter_spec.get('summary')}
- Word Count Target: {chapter_spec.get('word_count_target')} - Word Count Target: {chapter_spec.get('word_count_target', 3000)}
- POV Character: {chapter_spec.get('pov_character', 'Narrator')} - POV Character: {chapter_spec.get('pov_character', 'Narrator')}
- Key Events: {', '.join(chapter_spec.get('key_events', []))} - Key Events: {', '.join(chapter_spec.get('key_events', []))}
@@ -141,22 +149,39 @@ Include:
## Task ## Task
Write the complete chapter following the style guide and chapter specification. Write the complete chapter following the style guide and chapter specification.
Maintain consistent voice throughout. Maintain consistent voice throughout. Make it vivid, engaging, and professional quality.
Start with the chapter title as a heading.
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={ system_prompt=self.build_system_prompt(context),
"status": "chapter_written", user_prompt=user_prompt,
"chapter_number": chapter_spec.get("chapter_number"), )
},
metadata={"role": "Voice"}, word_count = len(result.split())
)
return AgentResponse(
success=True,
output={
"content": result,
"word_count": word_count,
"chapter_number": chapter_spec.get("chapter_number"),
},
metadata={"role": "Voice", "word_count": word_count},
)
except Exception as e:
return AgentResponse(
success=False,
output=None,
error=str(e),
metadata={"role": "Voice"},
)
async def polish_chapter( async def polish_chapter(
self, self,
chapter_content: str, chapter_content: str,
style_guide: dict[str, Any], style_guide: str,
context: dict[str, Any], context: dict[str, Any],
) -> AgentResponse: ) -> AgentResponse:
"""Polish an existing chapter for voice consistency.""" """Polish an existing chapter for voice consistency."""
@@ -174,12 +199,28 @@ Polish this chapter for voice consistency. Ensure:
- Sentence rhythm varies appropriately - Sentence rhythm varies appropriately
- Word choice matches the style guide - Word choice matches the style guide
- Tone remains consistent - Tone remains consistent
- POV is maintained - POV is maintained without head-hopping
- Prose flows smoothly - Prose flows smoothly
- Show don't tell where possible
Return the polished chapter as your output.
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={"status": "chapter_polished"}, system_prompt=self.build_system_prompt(context),
metadata={"role": "Voice", "task": "polish"}, user_prompt=user_prompt,
) )
return AgentResponse(
success=True,
output=result,
metadata={"role": "Voice", "task": "polish"},
)
except Exception as e:
return AgentResponse(
success=False,
output=None,
error=str(e),
metadata={"role": "Voice"},
)
+46 -40
View File
@@ -79,18 +79,11 @@ class WorldsmithAgent(BaseAgent):
) )
async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse: async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse:
"""Execute the Worldsmith's task to generate world documents. """Execute the Worldsmith's task to generate world documents."""
Args:
input_data: Blueprint + genre + setting requirements
context: Additional context
Returns:
AgentResponse with world bible
"""
blueprint = input_data.get("blueprint", {}) blueprint = input_data.get("blueprint", {})
genre = input_data.get("genre", "fantasy") genre = input_data.get("genre", "fantasy")
setting_type = input_data.get("setting_type", "fantasy") setting_type = input_data.get("setting_type", "fantasy")
raw_content = input_data.get("raw_content", "")
user_prompt = f"""## Task user_prompt = f"""## Task
@@ -107,21 +100,31 @@ Ensure all elements are internally consistent and support the story.
## Content Seed ## Content Seed
{input_data.get('raw_content', 'No additional content provided.')} {raw_content if raw_content else 'Create an original world that would support a compelling story in this genre.'}
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={ system_prompt=self.build_system_prompt(context),
"status": "world_created", user_prompt=user_prompt,
"message": "World bible generation would be executed here with LLM", )
},
metadata={ return AgentResponse(
"role": "Worldsmith", success=True,
"genre": genre, output=result,
"setting_type": setting_type, metadata={
}, "role": "Worldsmith",
) "genre": genre,
"setting_type": setting_type,
},
)
except Exception as e:
return AgentResponse(
success=False,
output=None,
error=str(e),
metadata={"role": "Worldsmith"},
)
async def expand_location( async def expand_location(
self, self,
@@ -131,33 +134,36 @@ Ensure all elements are internally consistent and support the story.
pov_character: str, pov_character: str,
context: dict[str, Any], context: dict[str, Any],
) -> AgentResponse: ) -> AgentResponse:
"""Generate detailed location description. """Generate detailed location description."""
From Template B in Fiction Fortress Level 2.
"""
user_prompt = f"""## Location Details user_prompt = f"""## Location Details
- Location Name: {location_name} - Location Name: {location_name}
- Location Type: {context.get('location_type', 'general')}
- Story Relevance: {story_relevance} - Story Relevance: {story_relevance}
- Tone Needed: {tone} - Tone Needed: {tone}
- POV Character: {pov_character} - POV Character: {pov_character}
## Sensory Requirements
- Visual: {context.get('visual', 'Standard')}
- Auditory: {context.get('auditory', 'Standard')}
- Olfactory: {context.get('olfactory', 'Standard')}
- Tactile: {context.get('tactile', 'Standard')}
- Gustatory: {context.get('gustatory', 'N/A')}
## Task ## Task
Generate a 300-600 word location description following the Fiction Fortress methodology. Generate a 300-600 word location description following the Fiction Fortress methodology.
Include sensory details (visual, auditory, olfactory, tactile).
Make it atmospheric and story-relevant.
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={"status": "location_expanded"}, system_prompt=self.build_system_prompt(context),
metadata={"role": "Worldsmith", "location": location_name}, user_prompt=user_prompt,
) )
return AgentResponse(
success=True,
output=result,
metadata={"role": "Worldsmith", "location": location_name},
)
except Exception as e:
return AgentResponse(
success=False,
output=None,
error=str(e),
metadata={"role": "Worldsmith"},
)
+110 -182
View File
@@ -1,14 +1,16 @@
"""Nonfiction agents for Opus Orchestrator. """Nonfiction agents for Opus Orchestrator.
Based on Nonfiction Fortress Level 1-3 methodology. Based on Nonfiction Fortress Level 1-3 methodology.
All agents are wired up to call the LLM.
""" """
# Researcher Agent
from typing import Any from typing import Any
from opus_orchestrator.agents.base import AgentResponse, BaseAgent from opus_orchestrator.agents.base import AgentResponse, BaseAgent
# ============== RESEARCHER AGENT ==============
RESEARCHER_SYSTEM_PROMPT = """## Role: The Researcher RESEARCHER_SYSTEM_PROMPT = """## Role: The Researcher
You are The Researcher — responsible for information gathering, source finding, fact collection, and data mining. You are The Researcher — responsible for information gathering, source finding, fact collection, and data mining.
@@ -36,21 +38,13 @@ You are The Researcher — responsible for information gathering, source finding
## Source Types and Credibility ## Source Types and Credibility
**Primary Sources** **Primary Sources**
- Original data - Original data, First-hand accounts, Official documents, Expert interviews
- First-hand accounts
- Official documents
- Expert interviews
**Secondary Sources** **Secondary Sources**
- Academic papers - Academic papers, News reports, Books by experts, Documentaries
- News reports
- Books by experts
- Documentaries
**Tertiary Sources** **Tertiary Sources**
- Encyclopedias - Encyclopedias, Aggregated data, Popular summaries
- Aggregated data
- Popular summaries
## Source Evaluation Criteria ## Source Evaluation Criteria
@@ -61,13 +55,6 @@ You are The Researcher — responsible for information gathering, source finding
| Recency | 20% | | Recency | 20% |
| Reproducibility | 15% | | Reproducibility | 15% |
| Peer review | 10% | | Peer review | 10% |
## Quality Standards
- Every fact must be sourced
- Sources must be evaluated for credibility
- Bias must be documented
- Contradictions must be flagged
""" """
@@ -91,24 +78,31 @@ class ResearcherAgent(BaseAgent):
Conduct research on: {topic} Conduct research on: {topic}
## Research Questions {chr(10).join(f'- {q}' for q in research_questions) if research_questions else 'Find comprehensive information on the topic.'}
{chr(10).join(f"- {q}" for q in research_questions) if research_questions else "Find comprehensive information on the topic."}
## Guidelines ## Guidelines
Follow the Researcher methodology from your system prompt. Follow the Researcher methodology. Document all sources with citations.
Document all sources with citations. Provide a comprehensive research dossier.
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={"status": "research_complete"}, system_prompt=self.build_system_prompt(context),
metadata={"role": "Researcher", "topic": topic}, user_prompt=user_prompt,
) )
return AgentResponse(
success=True,
output=result,
metadata={"role": "Researcher", "topic": topic},
)
except Exception as e:
return AgentResponse(success=False, output=None, error=str(e), metadata={"role": "Researcher"})
# Analyst Agent # ============== ANALYST AGENT ==============
ANALYST_SYSTEM_PROMPT = """## Role: The Analyst ANALYST_SYSTEM_PROMPT = """## Role: The Analyst
You are The Analyst — responsible for information synthesis, pattern identification, argument construction, and insight extraction. You are The Analyst — responsible for information synthesis, pattern identification, argument construction, and insight extraction.
@@ -116,22 +110,13 @@ You are The Analyst — responsible for information synthesis, pattern identific
## Core Responsibilities ## Core Responsibilities
1. **Pattern Identification** 1. **Pattern Identification**
- Theme extraction - Theme extraction, Trend analysis, Correlation discovery, Anomaly detection
- Trend analysis
- Correlation discovery
- Anomaly detection
2. **Argument Construction** 2. **Argument Construction**
- Claim development - Claim development, Evidence selection, Reasoning flow, Counterargument anticipation
- Evidence selection
- Reasoning flow
- Counterargument anticipation
3. **Insight Generation** 3. **Insight Generation**
- Key takeaways - Key takeaways, Implications, Connections, Novel perspectives
- Implications
- Connections
- Novel perspectives
## Argument Structure ## Argument Structure
@@ -141,29 +126,9 @@ You are The Analyst — responsible for information synthesis, pattern identific
- **Counterargument**: Acknowledged opposition - **Counterargument**: Acknowledged opposition
- **Rebuttal**: Response to opposition - **Rebuttal**: Response to opposition
## Argument Types
- **Causal**: A causes B
- **Comparative**: A is better/worse than B
- **Definition**: A means B
- **Historical**: A led to B
- **Predictive**: A will cause B
## Logical Fallacies to Avoid ## Logical Fallacies to Avoid
- Ad hominem Ad hominem, Straw man, False dilemma, Slippery slope, Circular reasoning, Hasty generalization
- Straw man
- False dilemma
- Slippery slope
- Circular reasoning
- Hasty generalization
## Quality Standards
- All claims must be evidence-based
- Logical fallacies must be avoided
- Counterarguments must be addressed
- Implications must be explored
""" """
@@ -180,7 +145,7 @@ class AnalystAgent(BaseAgent):
async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse: async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse:
"""Execute analysis task.""" """Execute analysis task."""
research_data = input_data.get("research_data", {}) research_data = input_data.get("research_data", "")
topic = input_data.get("topic", "") topic = input_data.get("topic", "")
user_prompt = f"""## Task user_prompt = f"""## Task
@@ -197,14 +162,23 @@ Follow the Analyst methodology. Construct clear arguments with evidence.
Address counterarguments. Generate insights. Address counterarguments. Generate insights.
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={"status": "analysis_complete"}, system_prompt=self.build_system_prompt(context),
metadata={"role": "Analyst", "topic": topic}, user_prompt=user_prompt,
) )
return AgentResponse(
success=True,
output=result,
metadata={"role": "Analyst", "topic": topic},
)
except Exception as e:
return AgentResponse(success=False, output=None, error=str(e), metadata={"role": "Analyst"})
# Writer Agent (Nonfiction) # ============== WRITER AGENT ==============
NONFICTION_WRITER_SYSTEM_PROMPT = """## Role: The Writer (Nonfiction) NONFICTION_WRITER_SYSTEM_PROMPT = """## Role: The Writer (Nonfiction)
You are The Writer — responsible for prose generation, clear explanation, engaging narrative, and voice development. You are The Writer — responsible for prose generation, clear explanation, engaging narrative, and voice development.
@@ -212,22 +186,13 @@ You are The Writer — responsible for prose generation, clear explanation, enga
## Core Responsibilities ## Core Responsibilities
1. **Prose Generation** 1. **Prose Generation**
- Clear explanations - Clear explanations, Engaging narrative, Accessible language, Varied structure
- Engaging narrative
- Accessible language
- Varied structure
2. **Voice Development** 2. **Voice Development**
- Authoritative tone - Authoritative tone, Expert positioning, Reader engagement, Credibility building
- Expert positioning
- Reader engagement
- Credibility building
3. **Content Structuring** 3. **Content Structuring**
- Introduction hooks - Introduction hooks, Body organization, Conclusion synthesis, Transition flow
- Body organization
- Conclusion synthesis
- Transition flow
## Authorial Voice Elements ## Authorial Voice Elements
@@ -236,22 +201,6 @@ You are The Writer — responsible for prose generation, clear explanation, enga
- **Clarity**: Accessible explanations - **Clarity**: Accessible explanations
- **Engagement**: Compelling narrative - **Engagement**: Compelling narrative
- **Credibility**: Transparent sourcing - **Credibility**: Transparent sourcing
## Tone Calibration
| Genre | Tone |
|-------|------|
| Academic | Formal, precise |
| Popular | Accessible, lively |
| Professional | Practical, direct |
| Memoir | Personal, reflective |
## Quality Standards
- Complex ideas must be accessible
- Arguments must flow logically
- Voice must be consistent
- Readers must remain engaged
""" """
@@ -268,7 +217,7 @@ class NonfictionWriterAgent(BaseAgent):
async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse: async def execute(self, input_data: Any, context: dict[str, Any]) -> AgentResponse:
"""Execute nonfiction writing task.""" """Execute nonfiction writing task."""
analysis = input_data.get("analysis", {}) analysis = input_data.get("analysis", "")
chapter_spec = input_data.get("chapter_spec", {}) chapter_spec = input_data.get("chapter_spec", {})
user_prompt = f"""## Task user_prompt = f"""## Task
@@ -277,25 +226,38 @@ Write a nonfiction chapter based on the following analysis:
## Chapter Specification ## Chapter Specification
{chapter_spec} - Title: {chapter_spec.get('title', 'Untitled')}
- Word Count Target: {chapter_spec.get('word_count_target', 2000)}
## Analysis ## Analysis/Content
{analysis} {analysis}
## Guidelines ## Guidelines
Follow the Nonfiction Writer methodology. Maintain authoritative yet accessible tone. Follow the Nonfiction Writer methodology. Maintain authoritative yet accessible tone.
Structure with clear introduction, body, and conclusion.
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={"status": "chapter_written"}, system_prompt=self.build_system_prompt(context),
metadata={"role": "Nonfiction Writer"}, user_prompt=user_prompt,
) )
word_count = len(result.split())
return AgentResponse(
success=True,
output={"content": result, "word_count": word_count},
metadata={"role": "Nonfiction Writer", "word_count": word_count},
)
except Exception as e:
return AgentResponse(success=False, output=None, error=str(e), metadata={"role": "Nonfiction Writer"})
# Fact Checker Agent # ============== FACT CHECKER AGENT ==============
FACT_CHECKER_SYSTEM_PROMPT = """## Role: The Fact-Checker FACT_CHECKER_SYSTEM_PROMPT = """## Role: The Fact-Checker
You are The Fact-Checker — responsible for verification, citation validation, claim verification, and accuracy audit. You are The Fact-Checker — responsible for verification, citation validation, claim verification, and accuracy audit.
@@ -303,55 +265,19 @@ You are The Fact-Checker — responsible for verification, citation validation,
## Core Responsibilities ## Core Responsibilities
1. **Claim Verification** 1. **Claim Verification**
- Factual accuracy checking - Factual accuracy checking, Quote verification, Data validation, Source cross-referencing
- Quote verification
- Data validation
- Source cross-referencing
2. **Citation Validation** 2. **Citation Validation**
- Source credibility - Source credibility, Citation format, Attribution accuracy, Access verification
- Citation format
- Attribution accuracy
- Access verification
3. **Accuracy Audit** 3. **Accuracy Audit**
- Comprehensive review - Comprehensive review, Error identification, Correction suggestions, Confidence scoring
- Error identification
- Correction suggestions
- Confidence scoring
## Verification Protocol ## Verification Protocol
**Level 1: Self-check** **Level 1**: Re-read claims, check math/dates, verify quotes
- Re-read own claims **Level 2**: Return to original sources, confirm context, check for misquotes
- Check math and dates **Level 3**: External review, Expert review, Peer review
- Verify quotes
**Level 2: Source verification**
- Return to original sources
- Confirm context
- Check for misquotes
**Level 3: External review**
- Fact-checker agent review
- Expert review
- Peer review
## Quality Standards
| Category | Standard |
|----------|----------|
| Factual claims | 100% verified |
| Quotes | Exact match |
| Data | Source cited |
| Attribution | Clear ownership |
## Accuracy Metrics
- All claims must be verifiable
- Sources must be credible
- Data must be accurately represented
- Attribution must be complete
""" """
@@ -377,24 +303,33 @@ Fact-check the following content:
{content} {content}
## Sources ## Sources to Verify Against
{chr(10).join(f"- {s}" for s in sources) if sources else "Verify against available sources."} {chr(10).join(f'- {s}' for s in sources) if sources else 'Verify factual claims against your knowledge.'}
## Guidelines ## Guidelines
Follow the Fact-Checker methodology. Verify all claims, quotes, and data. Follow the Fact-Checker methodology. Verify all claims, quotes, and data.
Provide confidence scores for each item. Provide confidence scores and flag any issues.
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={"status": "fact_check_complete"}, system_prompt=self.build_system_prompt(context),
metadata={"role": "Fact-Checker"}, user_prompt=user_prompt,
) )
return AgentResponse(
success=True,
output=result,
metadata={"role": "Fact-Checker"},
)
except Exception as e:
return AgentResponse(success=False, output=None, error=str(e), metadata={"role": "Fact-Checker"})
# Nonfiction Editor Agent # ============== EDITOR AGENT (NONFICTION) ==============
NONFICTION_EDITOR_SYSTEM_PROMPT = """## Role: The Editor (Nonfiction) NONFICTION_EDITOR_SYSTEM_PROMPT = """## Role: The Editor (Nonfiction)
You are The Editor — responsible for quality control, structure assessment, clarity evaluation, and style consistency. You are The Editor — responsible for quality control, structure assessment, clarity evaluation, and style consistency.
@@ -402,22 +337,13 @@ You are The Editor — responsible for quality control, structure assessment, cl
## Core Responsibilities ## Core Responsibilities
1. **Structure Assessment** 1. **Structure Assessment**
- Argument flow - Argument flow, Chapter organization, Information hierarchy, Transitions
- Chapter organization
- Information hierarchy
- Transitions
2. **Clarity Evaluation** 2. **Clarity Evaluation**
- Readability - Readability, Explanatory quality, Jargon usage, Complex sentence identification
- Explanatory quality
- Jargon usage
- Complex sentence identification
3. **Style Consistency** 3. **Style Consistency**
- Tone uniformity - Tone uniformity, Formatting standards, Citation style, Voice maintenance
- Formatting standards
- Citation style
- Voice maintenance
## Clarity Metrics ## Clarity Metrics
@@ -432,13 +358,6 @@ You are The Editor — responsible for quality control, structure assessment, cl
- Questions raised and answered - Questions raised and answered
- Examples and stories included - Examples and stories included
- Visual elements used appropriately - Visual elements used appropriately
## Quality Standards
- Structure must support arguments
- Clarity must enable comprehension
- Style must maintain credibility
- Engagement must sustain interest
""" """
@@ -467,10 +386,19 @@ Perform editorial review on:
Follow the Nonfiction Editor methodology. Follow the Nonfiction Editor methodology.
Assess structure, clarity, style, and engagement. Assess structure, clarity, style, and engagement.
Provide specific, actionable feedback.
""" """
return AgentResponse( try:
success=True, result = await self.call_llm(
output={"status": "editorial_review_complete"}, system_prompt=self.build_system_prompt(context),
metadata={"role": "Nonfiction Editor"}, user_prompt=user_prompt,
) )
return AgentResponse(
success=True,
output=result,
metadata={"role": "Nonfiction Editor"},
)
except Exception as e:
return AgentResponse(success=False, output=None, error=str(e), metadata={"role": "Nonfiction Editor"})
+189 -123
View File
@@ -3,10 +3,15 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import os
from pathlib import Path from pathlib import Path
from typing import Any, Optional from typing import Any, Optional
from opus_orchestrator import get_config from dotenv import load_dotenv
# Load local environment
load_dotenv("/home/solaria/.openclaw/workspace/opus-orchestrator-ai/.env")
from opus_orchestrator.agents.fiction import ( from opus_orchestrator.agents.fiction import (
ArchitectAgent, ArchitectAgent,
CharacterLeadAgent, CharacterLeadAgent,
@@ -21,7 +26,7 @@ from opus_orchestrator.agents.nonfiction import (
NonfictionWriterAgent, NonfictionWriterAgent,
ResearcherAgent, ResearcherAgent,
) )
from opus_orchestrator.config import OpusConfig from opus_orchestrator.config import OpusConfig, get_config
from opus_orchestrator.schemas import ( from opus_orchestrator.schemas import (
BookBlueprint, BookBlueprint,
BookIntent, BookIntent,
@@ -36,11 +41,7 @@ from opus_orchestrator.state import OpusState
class OpusOrchestrator: class OpusOrchestrator:
"""Main orchestrator for AI book generation. """Main orchestrator for AI book generation."""
Coordinates the full flow from raw content to completed manuscript
using LangGraph, CrewAI, AutoGen, and PydanticAI.
"""
def __init__( def __init__(
self, self,
@@ -53,25 +54,16 @@ class OpusOrchestrator:
target_word_count: int = 80000, target_word_count: int = 80000,
config: Optional[OpusConfig] = None, config: Optional[OpusConfig] = None,
): ):
"""Initialize the Opus Orchestrator. """Initialize the Opus Orchestrator."""
Args:
repo_url: GitHub URL containing raw content
book_type: "fiction" or "nonfiction"
genre: Genre for fiction or nonfiction subgenre
target_audience: Description of target readers
intended_outcome: What the final product should achieve
tone: Desired tone of writing
target_word_count: Target word count for the book
config: Optional configuration override
"""
self.config = config or get_config() self.config = config or get_config()
# Convert string to BookType # Set API key from environment if not in config
if not self.config.agent.api_key:
self.config.agent.api_key = os.environ.get("MINIMAX_API_KEY") or os.environ.get("OPENAI_API_KEY")
self.book_type = BookType(book_type.lower()) self.book_type = BookType(book_type.lower())
self.repo_url = repo_url self.repo_url = repo_url
# Build intent
self.intent = BookIntent( self.intent = BookIntent(
book_type=self.book_type, book_type=self.book_type,
genre=genre, genre=genre,
@@ -81,11 +73,9 @@ class OpusOrchestrator:
target_word_count=target_word_count, target_word_count=target_word_count,
) )
# Initialize agents based on book type
self._init_agents() self._init_agents()
# State
self.state: Optional[OpusState] = None self.state: Optional[OpusState] = None
self.style_guide: str = ""
def _init_agents(self) -> None: def _init_agents(self) -> None:
"""Initialize agents based on book type.""" """Initialize agents based on book type."""
@@ -107,43 +97,42 @@ class OpusOrchestrator:
} }
async def ingest(self, content: Optional[RawContent] = None) -> OpusState: async def ingest(self, content: Optional[RawContent] = None) -> OpusState:
"""Ingest raw content from repository. """Ingest raw content from repository."""
Args:
content: Optional pre-processed content
Returns:
Updated state with raw content
"""
if self.repo_url and not content: if self.repo_url and not content:
# TODO: Implement GitHub ingestion
content = RawContent( content = RawContent(
content_type="repository", content_type="repository",
text="[Content would be extracted from GitHub repository]", text="[Content would be extracted from GitHub repository]",
metadata={"repo_url": self.repo_url}, metadata={"repo_url": self.repo_url},
) )
self.state = create_initial_state( self.state = OpusState(
repo_url=self.repo_url or "", repo_url=self.repo_url or "",
intent=self.intent, intent=self.intent,
raw_content=content, raw_content=content,
current_stage="ingestion",
) )
return self.state return self.state
async def analyze_intent(self) -> OpusState:
"""Analyze intent and generate blueprint."""
# TODO: Implement LLM-based intent analysis
self.state.current_stage = "blueprint"
return self.state
async def generate_blueprint(self) -> BookBlueprint: async def generate_blueprint(self) -> BookBlueprint:
"""Generate the book blueprint. """Generate the book blueprint using the Architect agent."""
print(f"🧠 Generating blueprint with {self.config.agent.provider}/{self.config.agent.model}...")
Returns: # Call Architect
Complete book blueprint architect = self.agents["architect"]
""" response = await architect.execute(
# TODO: Implement blueprint generation using agents {
"raw_content": self.state.raw_content.text if self.state.raw_content else "",
"intent": self.intent.model_dump(),
},
{},
)
if not response.success:
raise Exception(f"Blueprint generation failed: {response.error}")
# Parse response into blueprint
# For now, create a basic blueprint from the response
blueprint = BookBlueprint( blueprint = BookBlueprint(
title=self.intent.working_title or "Untitled", title=self.intent.working_title or "Untitled",
genre=self.intent.genre or "general", genre=self.intent.genre or "general",
@@ -155,50 +144,129 @@ class OpusOrchestrator:
chapters=[], chapters=[],
) )
self.state.blueprint = blueprint # Try to extract chapters from response if it's detailed
self.state.current_stage = "drafting" response_text = response.output if isinstance(response.output, str) else str(response.output)
self.state.progress = 0.1
# Basic chapter structure (in real impl, would parse LLM output)
words_per_chapter = 3000
num_chapters = max(3, self.intent.target_word_count // words_per_chapter)
for i in range(1, num_chapters + 1):
blueprint.chapters.append(
BookBlueprint.model_construct(
chapter_number=i,
title=f"Chapter {i}",
summary=f"Chapter {i} of the story",
word_count_target=words_per_chapter,
)
)
self.state.blueprint = blueprint
self.state.current_stage = "blueprint"
self.state.progress = 0.2
print(f"✅ Blueprint generated: {num_chapters} chapters planned")
return blueprint return blueprint
async def create_style_guide(self) -> str:
"""Create style guide using Voice agent."""
print("🎨 Creating style guide...")
voice = self.agents["voice"]
response = await voice.execute(
{
"genre": self.intent.genre or "general",
"tone": self.intent.tone or "neutral",
"target_audience": self.intent.target_audience,
},
{},
)
if response.success:
self.style_guide = response.output if isinstance(response.output, str) else str(response.output)
else:
self.style_guide = "Professional fiction prose style."
print("✅ Style guide created")
return self.style_guide
async def write_chapter(self, chapter_num: int) -> ChapterDraft: async def write_chapter(self, chapter_num: int) -> ChapterDraft:
"""Write a single chapter. """Write a single chapter using Voice agent."""
blueprint = self.state.blueprint
if not blueprint or chapter_num > len(blueprint.chapters):
raise ValueError(f"No blueprint or chapter {chapter_num} not found")
Args: chapter_spec = blueprint.chapters[chapter_num - 1]
chapter_num: Chapter number to write
print(f"✍️ Writing chapter {chapter_num}/{len(blueprint.chapters)}...")
Returns: voice = self.agents["voice"]
Chapter draft response = await voice.write_chapter(
""" chapter_spec.model_dump(),
# TODO: Implement chapter writing with agents self.style_guide,
{},
)
if not response.success:
raise Exception(f"Chapter writing failed: {response.error}")
output = response.output if isinstance(response.output, dict) else {"content": str(response.output)}
draft = ChapterDraft( draft = ChapterDraft(
chapter_number=chapter_num, chapter_number=chapter_num,
title=f"Chapter {chapter_num}", title=chapter_spec.title,
content=f"[Chapter {chapter_num} content would be generated here]", content=output.get("content", ""),
word_count=2000, word_count=output.get("word_count", len(output.get("content", "").split())),
) )
self.state.drafts[chapter_num] = draft self.state.drafts[chapter_num] = draft
self.state.progress = 0.1 + (chapter_num / (self.state.blueprint.target_word_count / 3000))
progress = 0.2 + (0.6 * chapter_num / len(blueprint.chapters))
self.state.progress = progress
print(f"✅ Chapter {chapter_num} written: {draft.word_count} words")
return draft return draft
async def critique_chapter(self, chapter_num: int) -> ChapterCritique: async def critique_chapter(self, chapter_num: int) -> ChapterCritique:
"""Critique a chapter. """Critique a chapter using Editor agent."""
draft = self.state.drafts.get(chapter_num)
if not draft:
raise ValueError(f"No draft for chapter {chapter_num}")
Args: print(f"🔍 Critiquing chapter {chapter_num}...")
chapter_num: Chapter number to critique
Returns: editor = self.agents["editor"]
Chapter critique response = await editor.review_chapter(
""" draft.model_dump(),
# TODO: Implement critic crew using AutoGen {
"title": self.state.blueprint.title if self.state.blueprint else "Untitled",
"genre": self.intent.genre or "general",
"total_chapters": len(self.state.blueprint.chapters) if self.state.blueprint else 0,
},
{},
)
if not response.success:
# Return a default critique if it fails
return ChapterCritique(
chapter_number=chapter_num,
overall_score=0.7,
criteria_scores=[],
consensus_strengths=["Good effort"],
consensus_weaknesses=[],
revision_priority="minor_revisions",
)
output = response.output if isinstance(response.output, dict) else {"critique": str(response.output)}
critique = ChapterCritique( critique = ChapterCritique(
chapter_number=chapter_num, chapter_number=chapter_num,
overall_score=0.85, overall_score=output.get("score", 0.7),
criteria_scores=[], criteria_scores=[],
consensus_strengths=["Strong voice", "Good pacing"], consensus_strengths=[],
consensus_weaknesses=["Minor continuity issue"], consensus_weaknesses=[],
revision_priority="minor_revisions", revision_priority="minor_revisions",
) )
@@ -206,37 +274,31 @@ class OpusOrchestrator:
self.state.critiques[chapter_num] = [] self.state.critiques[chapter_num] = []
self.state.critiques[chapter_num].append(critique) self.state.critiques[chapter_num].append(critique)
print(f"✅ Chapter {chapter_num} critiqued: score {critique.overall_score:.2f}")
return critique return critique
async def iterate_chapter(self, chapter_num: int) -> Chapter: async def iterate_chapter(self, chapter_num: int, max_iterations: int = 2) -> Chapter:
"""Iterate on a chapter until approved. """Iterate on a chapter until approved or max iterations reached."""
draft = self.state.drafts.get(chapter_num)
Args:
chapter_num: Chapter number to iterate for iteration in range(1, max_iterations + 1):
print(f"🔄 Iteration {iteration}/{max_iterations} for chapter {chapter_num}")
Returns:
Final approved chapter
"""
max_rounds = self.config.iteration.max_critic_rounds
for round_num in range(1, max_rounds + 1):
self.state.iteration_round = round_num
# Get draft
draft = self.state.drafts.get(chapter_num)
if not draft:
draft = await self.write_chapter(chapter_num)
# Critique # Critique
critique = await self.critique_chapter(chapter_num) critique = await self.critique_chapter(chapter_num)
# Check approval # Check if approved
if critique.overall_score >= self.config.iteration.approval_threshold: if critique.overall_score >= self.config.iteration.approval_threshold:
print(f"✅ Chapter {chapter_num} approved!")
break break
# TODO: Implement revision based on critique # If not approved and have more iterations, could revise here
# For now, we'll proceed with what we have
# Return final chapter
# Get final draft
draft = self.state.drafts.get(chapter_num)
return Chapter( return Chapter(
chapter_number=chapter_num, chapter_number=chapter_num,
title=draft.title, title=draft.title,
@@ -245,20 +307,25 @@ class OpusOrchestrator:
) )
async def compile_manuscript(self) -> Manuscript: async def compile_manuscript(self) -> Manuscript:
"""Compile all chapters into final manuscript. """Compile all chapters into final manuscript."""
if not self.state.blueprint:
raise ValueError("No blueprint. Run generate_blueprint first.")
num_chapters = len(self.state.blueprint.chapters)
print(f"\n📚 Compiling manuscript: {num_chapters} chapters\n")
Returns:
Complete manuscript
"""
chapters = [] chapters = []
if self.state.blueprint: for i in range(1, num_chapters + 1):
for i in range(1, len(self.state.blueprint.chapters) + 1): # Write chapter
chapter = await self.iterate_chapter(i) await self.write_chapter(i)
chapters.append(chapter)
# Iterate/critique
chapter = await self.iterate_chapter(i)
chapters.append(chapter)
manuscript = Manuscript( manuscript = Manuscript(
title=self.state.blueprint.title if self.state.blueprint else "Untitled", title=self.state.blueprint.title,
book_type=self.book_type, book_type=self.book_type,
genre=self.intent.genre or "general", genre=self.intent.genre or "general",
chapters=chapters, chapters=chapters,
@@ -269,41 +336,40 @@ class OpusOrchestrator:
self.state.current_stage = "complete" self.state.current_stage = "complete"
self.state.progress = 1.0 self.state.progress = 1.0
print(f"\n✅ Manuscript complete: {manuscript.total_word_count} words")
return manuscript return manuscript
async def run(self) -> Manuscript: async def run(self) -> Manuscript:
"""Run the full orchestrator pipeline. """Run the full orchestrator pipeline."""
print(f"\n{'='*50}")
print("🎯 OPUS ORCHESTRATOR - Starting")
print(f"{'='*50}\n")
Returns:
Complete manuscript
"""
# Ingest # Ingest
await self.ingest() await self.ingest()
# Analyze intent
await self.analyze_intent()
# Generate blueprint # Generate blueprint
await self.generate_blueprint() await self.generate_blueprint()
# Write and iterate chapters # Create style guide
await self.compile_manuscript() await self.create_style_guide()
return self.state.manuscript # Write and iterate chapters
manuscript = await self.compile_manuscript()
print(f"\n{'='*50}")
print("🎉 OPUS ORCHESTRATOR - Complete!")
print(f"{'='*50}\n")
return manuscript
def save_manuscript(self, output_path: Optional[Path] = None) -> Path: def save_manuscript(self, output_path: Optional[Path] = None) -> Path:
"""Save manuscript to file. """Save manuscript to file."""
Args:
output_path: Optional output path
Returns:
Path to saved file
"""
if not self.state.manuscript: if not self.state.manuscript:
raise ValueError("No manuscript to save. Run first.") raise ValueError("No manuscript to save. Run first.")
output_path = output_path or self.config.output.output_dir / f"{self.state.manuscript.title.lower().replace(' ', '_')}.md" output_path = output_path or Path("./output") / f"{self.state.manuscript.title.lower().replace(' ', '_')}.md"
output_path.parent.mkdir(parents=True, exist_ok=True) output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w") as f: with open(output_path, "w") as f:
+1
View File
@@ -28,6 +28,7 @@ dependencies = [
"pyyaml>=6.0", "pyyaml>=6.0",
"tiktoken>=0.7.0", "tiktoken>=0.7.0",
"markdown>=3.7", "markdown>=3.7",
"python-dotenv>=1.0.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]