Merge: Team 5 - Features (resolved conflict)
This commit is contained in:
@@ -7,7 +7,7 @@ import os
|
||||
from typing import Any, Optional
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, HTTPException, BackgroundTasks, UploadFile, File
|
||||
from fastapi import FastAPI, HTTPException, BackgroundTasks, StreamingResponse
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from dotenv import load_dotenv
|
||||
@@ -253,6 +253,54 @@ async def generate(request: GenerateRequest, background_tasks: BackgroundTasks):
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
|
||||
@app.post("/generate/stream", tags=["generate"])
|
||||
async def generate_stream(request: GenerateRequest):
|
||||
"""Generate a manuscript with streaming progress updates.
|
||||
|
||||
Returns Server-Sent Events (SSE) with progress updates.
|
||||
"""
|
||||
import traceback
|
||||
import json
|
||||
|
||||
async def event_generator():
|
||||
try:
|
||||
# Yield start event
|
||||
yield "data: " + json.dumps({"status": "starting", "message": "Initializing..."}) + "\n\n"
|
||||
|
||||
# Prepare seed concept
|
||||
seed_concept = request.concept
|
||||
|
||||
if request.repo:
|
||||
yield "data: " + json.dumps({"status": "ingesting", "message": "Fetching from GitHub..."}) + "\n\n"
|
||||
orch = OpusOrchestrator(
|
||||
book_type=request.book_type,
|
||||
genre=request.genre,
|
||||
target_word_count=request.target_word_count,
|
||||
framework=request.framework,
|
||||
)
|
||||
content = orch.ingest_from_github(request.repo)
|
||||
seed_concept = content.text
|
||||
yield "data: " + json.dumps({"status": "ingested", "message": f"Ingested {len(seed_concept)} characters"}) + "\n\n"
|
||||
|
||||
if not seed_concept:
|
||||
raise HTTPException(status_code=400, detail="Must provide concept or repo")
|
||||
|
||||
# For now, just stream a completion message
|
||||
# Full streaming requires modifying the LangGraph workflow
|
||||
yield "data: " + json.dumps({"status": "generating", "progress": 0.1, "message": "Starting generation..."}) + "\n\n"
|
||||
|
||||
# TODO: Implement actual streaming from LangGraph workflow
|
||||
# This requires modifying run_opus to yield progress events
|
||||
yield "data: " + json.dumps({"status": "generating", "progress": 0.5, "message": "Generating manuscript..."}) + "\n\n"
|
||||
|
||||
yield "data: " + json.dumps({"status": "complete", "progress": 1.0, "message": "Generation complete"}) + "\n\n"
|
||||
|
||||
except Exception as e:
|
||||
yield "data: " + json.dumps({"status": "error", "message": str(e)}) + "\n\n"
|
||||
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
|
||||
@app.post("/ingest", response_model=IngestResponse, tags=["ingest"])
|
||||
async def ingest(request: IngestRequest):
|
||||
"""Ingest content from a GitHub repository."""
|
||||
|
||||
+190
-114
@@ -1,127 +1,203 @@
|
||||
"""Tests for Opus Orchestrator."""
|
||||
"""Test suite for Opus Orchestrator.
|
||||
|
||||
Tests for core functionality - these can run without API keys.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from opus_orchestrator import OpusOrchestrator, BookType, BookIntent
|
||||
from opus_orchestrator.schemas import RawContent, Chapter, Manuscript
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
from opus_orchestrator.config import (
|
||||
OpusConfig,
|
||||
AgentConfig,
|
||||
CostConfig,
|
||||
IterationConfig,
|
||||
load_config_from_env,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def basic_intent():
|
||||
return BookIntent(
|
||||
book_type=BookType.FICTION,
|
||||
genre="science-fiction",
|
||||
target_audience="adult sci-fi readers",
|
||||
intended_outcome="complete novel ~80k words",
|
||||
tone="epic",
|
||||
target_word_count=80000,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def basic_content():
|
||||
return RawContent(
|
||||
content_type="outline",
|
||||
text="A space explorer discovers a new civilization...",
|
||||
)
|
||||
|
||||
|
||||
class TestOpusOrchestrator:
|
||||
"""Test suite for OpusOrchestrator."""
|
||||
|
||||
def test_init_fiction(self, basic_intent):
|
||||
"""Test initialization with fiction."""
|
||||
orch = OpusOrchestrator(
|
||||
book_type="fiction",
|
||||
genre="science-fiction",
|
||||
target_audience="adult sci-fi readers",
|
||||
intended_outcome="complete novel",
|
||||
)
|
||||
|
||||
assert orch.book_type == BookType.FICTION
|
||||
assert "architect" in orch.agents
|
||||
assert "voice" in orch.agents
|
||||
|
||||
def test_init_nonfiction(self):
|
||||
"""Test initialization with nonfiction."""
|
||||
orch = OpusOrchestrator(
|
||||
book_type="nonfiction",
|
||||
genre="business",
|
||||
target_audience="professionals",
|
||||
intended_outcome="complete business book",
|
||||
)
|
||||
|
||||
assert orch.book_type == BookType.NONFICTION
|
||||
assert "researcher" in orch.agents
|
||||
assert "analyst" in orch.agents
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ingest(self, basic_content):
|
||||
"""Test content ingestion."""
|
||||
orch = OpusOrchestrator(
|
||||
book_type="fiction",
|
||||
genre="fantasy",
|
||||
target_audience="general",
|
||||
intended_outcome="novel",
|
||||
)
|
||||
|
||||
state = await orch.ingest(basic_content)
|
||||
|
||||
assert state.raw_content == basic_content
|
||||
assert state.current_stage == "ingestion"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_blueprint(self, basic_content):
|
||||
"""Test blueprint generation."""
|
||||
orch = OpusOrchestrator(
|
||||
book_type="fiction",
|
||||
genre="mystery",
|
||||
target_audience="general",
|
||||
intended_outcome="novel",
|
||||
)
|
||||
|
||||
await orch.ingest(basic_content)
|
||||
blueprint = await orch.generate_blueprint()
|
||||
|
||||
assert blueprint.title == "Untitled"
|
||||
assert blueprint.target_word_count == 80000
|
||||
class TestConfig:
|
||||
"""Tests for configuration."""
|
||||
|
||||
def test_default_config(self):
|
||||
"""Test default configuration is valid."""
|
||||
config = OpusConfig()
|
||||
assert config.agent.model is not None
|
||||
assert config.agent.temperature == 0.7
|
||||
assert config.iteration.max_critic_rounds == 5
|
||||
|
||||
def test_cost_config_defaults(self):
|
||||
"""Test cost config has defaults."""
|
||||
cost = CostConfig()
|
||||
assert cost.track_usage is True
|
||||
assert "gpt-4o" in cost.price_per_million_tokens
|
||||
|
||||
def test_iteration_config(self):
|
||||
"""Test iteration config."""
|
||||
iteration = IterationConfig()
|
||||
assert iteration.approval_threshold == 0.8
|
||||
assert iteration.auto_proceed_threshold == 0.9
|
||||
assert iteration.max_critic_rounds >= iteration.min_critic_rounds
|
||||
|
||||
|
||||
class TestSchemas:
|
||||
"""Test schema validation."""
|
||||
|
||||
def test_book_intent_fiction(self):
|
||||
"""Test BookIntent for fiction."""
|
||||
"""Tests for Pydantic schemas."""
|
||||
|
||||
def test_book_intent_validation(self):
|
||||
"""Test BookIntent validation."""
|
||||
from opus_orchestrator.schemas import BookIntent, BookType
|
||||
|
||||
intent = BookIntent(
|
||||
book_type=BookType.FICTION,
|
||||
genre="thriller",
|
||||
target_audience="adult thriller readers",
|
||||
intended_outcome="complete thriller novel",
|
||||
target_word_count=90000,
|
||||
)
|
||||
|
||||
assert intent.book_type == BookType.FICTION
|
||||
assert intent.target_word_count == 90000
|
||||
|
||||
def test_manuscript_to_markdown(self):
|
||||
"""Test manuscript markdown conversion."""
|
||||
manuscript = Manuscript(
|
||||
title="Test Book",
|
||||
subtitle="A Test",
|
||||
book_type=BookType.FICTION,
|
||||
genre="fantasy",
|
||||
chapters=[
|
||||
Chapter(chapter_number=1, title="The Beginning", content="Content 1", word_count=1000),
|
||||
Chapter(chapter_number=2, title="The Middle", content="Content 2", word_count=1500),
|
||||
],
|
||||
total_word_count=2500,
|
||||
frontmatter={"include_toc": True, "dedication": "To test"},
|
||||
target_audience="young adult",
|
||||
intended_outcome="complete novel",
|
||||
target_word_count=80000,
|
||||
)
|
||||
|
||||
assert intent.book_type == BookType.FICTION
|
||||
assert intent.target_word_count == 80000
|
||||
|
||||
def test_chapter_blueprint(self):
|
||||
"""Test ChapterBlueprint validation."""
|
||||
from opus_orchestrator.schemas import ChapterBlueprint
|
||||
|
||||
chapter = ChapterBlueprint(
|
||||
chapter_number=1,
|
||||
title="The Beginning",
|
||||
summary="Our hero starts their journey",
|
||||
word_count_target=3000,
|
||||
)
|
||||
|
||||
assert chapter.chapter_number == 1
|
||||
assert chapter.word_count_target == 3000
|
||||
|
||||
md = manuscript.to_markdown()
|
||||
|
||||
assert "# Test Book" in md
|
||||
assert "## A Test" in md
|
||||
assert "## Table of Contents" in md
|
||||
assert "Chapter 1: The Beginning" in md
|
||||
assert "Chapter 2: The Middle" in md
|
||||
assert "*To test*" in md
|
||||
class TestFrameworks:
|
||||
"""Tests for story frameworks."""
|
||||
|
||||
def test_get_framework_prompt(self):
|
||||
"""Test framework prompt generation."""
|
||||
from opus_orchestrator.frameworks import get_framework_prompt, StoryFramework
|
||||
|
||||
# Test all frameworks have prompts
|
||||
for framework in StoryFramework:
|
||||
prompt = get_framework_prompt(framework)
|
||||
assert prompt is not None
|
||||
assert len(prompt) > 0
|
||||
|
||||
def test_framework_for_genre(self):
|
||||
"""Test framework suggestions by genre."""
|
||||
from opus_orchestrator.frameworks import get_framework_for_genre
|
||||
|
||||
# Fantasy should suggest Hero's Journey
|
||||
suggestions = get_framework_for_genre("fantasy")
|
||||
assert len(suggestions) > 0
|
||||
|
||||
# Unknown genre should fallback
|
||||
suggestions = get_framework_for_genre("unknown")
|
||||
assert len(suggestions) > 0
|
||||
|
||||
|
||||
class TestGitHubIngestor:
|
||||
"""Tests for GitHub ingestion."""
|
||||
|
||||
def test_ingestor_without_token(self):
|
||||
"""Test GitHubIngestor works without token."""
|
||||
from opus_orchestrator.utils.github_ignest import GitHubIngestor
|
||||
|
||||
# Should not raise without token
|
||||
ingestor = GitHubIngestor(token=None)
|
||||
assert ingestor.headers is not None
|
||||
assert "Accept" in ingestor.headers
|
||||
|
||||
def test_ingestor_with_token(self):
|
||||
"""Test GitHubIngestor with token."""
|
||||
from opus_orchestrator.utils.github_ignest import GitHubIngestor
|
||||
|
||||
ingestor = GitHubIngestor(token="test_token")
|
||||
assert "Authorization" in ingestor.headers
|
||||
|
||||
|
||||
class TestAgentResponse:
|
||||
"""Tests for agent responses."""
|
||||
|
||||
def test_agent_response_success(self):
|
||||
"""Test successful agent response."""
|
||||
from opus_orchestrator.agents.base import AgentResponse
|
||||
|
||||
response = AgentResponse(
|
||||
success=True,
|
||||
output="Test output",
|
||||
metadata={"key": "value"},
|
||||
)
|
||||
|
||||
assert response.success is True
|
||||
assert response.output == "Test output"
|
||||
assert response.error is None
|
||||
|
||||
def test_agent_response_error(self):
|
||||
"""Test error agent response."""
|
||||
from opus_orchestrator.agents.base import AgentResponse
|
||||
|
||||
response = AgentResponse(
|
||||
success=False,
|
||||
output=None,
|
||||
error="Something went wrong",
|
||||
)
|
||||
|
||||
assert response.success is False
|
||||
assert response.error == "Something went wrong"
|
||||
|
||||
|
||||
# Mock tests that require API keys
|
||||
class TestLLMClient:
|
||||
"""Tests for LLM client (mocked)."""
|
||||
|
||||
@patch('opus_orchestrator.utils.llm.requests.post')
|
||||
def test_sync_client_openai(self, mock_post):
|
||||
"""Test synchronous OpenAI client."""
|
||||
from opus_orchestrator.utils.llm import LLMClient
|
||||
|
||||
# Mock response
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"choices": [{"message": {"content": "Test response"}}]
|
||||
}
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
client = LLMClient(
|
||||
api_key="test_key",
|
||||
provider="openai",
|
||||
model="gpt-4o",
|
||||
)
|
||||
|
||||
result = client.complete(
|
||||
system_prompt="System",
|
||||
user_prompt="User",
|
||||
)
|
||||
|
||||
assert result == "Test response"
|
||||
mock_post.assert_called_once()
|
||||
|
||||
|
||||
# Integration-like tests (need environment)
|
||||
class TestIntegration:
|
||||
"""Integration tests - skip if no API key."""
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not __import__('os')..environ.get('OPENAI_API_KEY'),
|
||||
reason="No API key"
|
||||
)
|
||||
def test_real_api_call(self):
|
||||
"""Test actual API call if key exists."""
|
||||
from opus_orchestrator.utils.llm import LLMClient
|
||||
|
||||
client = LLMClient(provider="openai", model="gpt-4o")
|
||||
|
||||
result = client.complete(
|
||||
system_prompt="You are a helpful assistant.",
|
||||
user_prompt="Say 'test' if you receive this.",
|
||||
)
|
||||
|
||||
assert "test" in result.lower()
|
||||
|
||||
Reference in New Issue
Block a user