| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- import sys
- import os
- import json
- import uvicorn
- from fastapi import FastAPI, HTTPException, Body
- from fastapi.middleware.cors import CORSMiddleware
- from pydantic import BaseModel
- from typing import List, Optional, Dict, Any
- # Add parent directory to sys.path to import agents
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
- # Add agents directory to sys.path so internal imports in agents work
- sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../agents")))
- from agents.outline_agent import OutlineAgent
- from agents.chapter_generate_agent import ChapterGenerateAgent
- from hello_agents import HelloAgentsLLM
- app = FastAPI()
- # Enable CORS for frontend
- app.add_middleware(
- CORSMiddleware,
- allow_origins=["*"], # Allows all origins
- allow_credentials=True,
- allow_methods=["*"], # Allows all methods
- allow_headers=["*"], # Allows all headers
- )
- # Data Models
- class OutlineRequest(BaseModel):
- novel_id: str
- title: str
- user_input: str
- tags: Optional[List[str]] = []
- target_length: Optional[int] = 3000
- style_tags: Dict[str, str] = {} # e.g. {"style": "dark", "tone": "serious"}
- class OutlineUpdateRequest(BaseModel):
- novel_id: str
- title: str
- note_id: str
- content: str
- tags: Optional[List[str]] = None
- class ChapterGenerateRequest(BaseModel):
- novel_id: str
- title: str
- user_input: str
- num_chapters: int = 1
- chapter_length: int = 3000
- class ChapterUpdateRequest(BaseModel):
- novel_id: str
- title: str
- note_id: str
- content: Optional[str] = None
- chapter_title: Optional[str] = None
- summary: Optional[str] = None
- next_chapter_prediction: Optional[str] = None
- # Manager
- class ProjectManager:
- def __init__(self, workspace="./outputs"):
- self.workspace = workspace
- if not os.path.exists(workspace):
- os.makedirs(workspace)
- def get_project_dir(self, title, novel_id):
- return os.path.join(self.workspace, f"{title}-{novel_id}")
- def get_mapping_file(self, title, novel_id):
- return os.path.join(self.get_project_dir(title, novel_id), "project_data.json")
- def load_mapping(self, title, novel_id):
- path = self.get_mapping_file(title, novel_id)
- if os.path.exists(path):
- with open(path, "r", encoding="utf-8") as f:
- return json.load(f)
- return {"novel_id": novel_id, "title": title, "outline_id": None, "chapters": []}
- def save_mapping(self, title, novel_id, data):
- path = self.get_mapping_file(title, novel_id)
- project_dir = self.get_project_dir(title, novel_id)
- if not os.path.exists(project_dir):
- os.makedirs(project_dir)
- with open(path, "w", encoding="utf-8") as f:
- json.dump(data, f, indent=2, ensure_ascii=False)
- def update_outline_mapping(self, title, novel_id, outline_id):
- data = self.load_mapping(title, novel_id)
- data["outline_id"] = outline_id
- self.save_mapping(title, novel_id, data)
- def add_chapter_mapping(self, title, novel_id, chapter_data):
- data = self.load_mapping(title, novel_id)
- data["chapters"].append(chapter_data)
- self.save_mapping(title, novel_id, data)
- def update_chapter_mapping(self, title, novel_id, note_id, update_data):
- data = self.load_mapping(title, novel_id)
- for chapter in data["chapters"]:
- if chapter["id"] == note_id:
- chapter.update(update_data)
- break
- self.save_mapping(title, novel_id, data)
- def remove_chapter_mapping(self, title, novel_id, note_id):
- data = self.load_mapping(title, novel_id)
- data["chapters"] = [c for c in data["chapters"] if c["id"] != note_id]
- self.save_mapping(title, novel_id, data)
- project_manager = ProjectManager()
- # Agents
- llm_instance = HelloAgentsLLM(model=os.getenv("LLM_MODEL_ID"))
- outline_agent = OutlineAgent(name="OutlineAgent", llm=llm_instance, workspace="./outputs")
- chapter_agent = ChapterGenerateAgent(
- name="ChapterAgent",
- llm=llm_instance,
- workspace="./outputs",
- chapter_length=3000 # Default length, can be overridden in run
- )
- # API Endpoints
- @app.get("/projects/{title}/{novel_id}")
- def get_project_data(title: str, novel_id: str):
- return project_manager.load_mapping(title, novel_id)
- # --- Outline ---
- @app.post("/outline/generate")
- def generate_outline(req: OutlineRequest):
- # Construct kwargs for run
- run_kwargs = {
- "novel_id": req.novel_id,
- "title": req.title,
- "target_length": req.target_length
- }
- run_kwargs.update(req.style_tags)
-
- response, note_id = outline_agent.run(req.user_input, **run_kwargs)
-
- project_manager.update_outline_mapping(req.title, req.novel_id, note_id)
-
- return {"note_id": note_id, "content": response}
- @app.get("/outline/{title}/{novel_id}/{note_id}")
- def get_outline(title: str, novel_id: str, note_id: str):
- content = outline_agent.get_outline(novel_id, note_id, title=title)
- # Remove frontmatter if present (simple check)
- # NoteTool returns raw content usually.
- # Frontmatter format: --- ... ---
- if content.startswith("---"):
- parts = content.split("---", 2)
- if len(parts) >= 3:
- content = parts[2].strip()
- return {"content": content}
- @app.put("/outline/update")
- def update_outline(req: OutlineUpdateRequest):
- outline_agent.update_outline(req.novel_id, req.note_id, title=req.title, content=req.content, tags=req.tags)
- return {"status": "success"}
- @app.delete("/outline/delete")
- def delete_outline(novel_id: str, title: str, note_id: str):
- outline_agent.del_outline(novel_id, note_id, title=title)
-
- data = project_manager.load_mapping(title, novel_id)
- if data["outline_id"] == note_id:
- data["outline_id"] = None
- project_manager.save_mapping(title, novel_id, data)
- return {"status": "success"}
- # --- Chapters ---
- @app.post("/chapter/generate")
- def generate_chapters(req: ChapterGenerateRequest):
- generated_chapters = []
- current_input = req.user_input
-
- for i in range(req.num_chapters):
- try:
- chapter_data, note_id = chapter_agent.run(
- user_input=current_input,
- novel_id=req.novel_id,
- novel_title=req.title,
- chapter_length=req.chapter_length
- )
-
- # Clear input for subsequent chapters to rely on context/prediction
- if i == 0:
- current_input = ""
-
- chapter_info = {
- "id": note_id,
- "title": chapter_data.get("title", "Unknown"),
- "summary": chapter_data.get("summary", "")
- }
- generated_chapters.append(chapter_info)
- project_manager.add_chapter_mapping(req.title, req.novel_id, chapter_info)
- except Exception as e:
- print(f"Error generating chapter {i+1}: {e}")
- # Stop generating if one fails? Or continue?
- # Probably stop and return what we have.
- break
-
- return {"generated_chapters": generated_chapters}
- @app.get("/chapter/{title}/{novel_id}/{note_id}")
- def get_chapter(title: str, novel_id: str, note_id: str):
- path = os.path.join("./outputs", f"{title}-{novel_id}", "chapters", f"{note_id}.md")
- if os.path.exists(path):
- with open(path, "r", encoding="utf-8") as f:
- content = f.read()
-
- # Remove frontmatter
- if content.startswith("---"):
- parts = content.split("---", 2)
- if len(parts) >= 3:
- content = parts[2].strip()
-
- return {"content": content}
- raise HTTPException(status_code=404, detail="Chapter not found")
- @app.put("/chapter/update")
- def update_chapter(req: ChapterUpdateRequest):
- update_kwargs = {}
- if req.content is not None:
- update_kwargs["content"] = req.content
- if req.chapter_title is not None:
- update_kwargs["title"] = req.chapter_title
- if req.summary is not None:
- update_kwargs["summary"] = req.summary
- if req.next_chapter_prediction is not None:
- update_kwargs["next_chapter_prediction"] = req.next_chapter_prediction
-
- chapter_agent.update_chapter(req.novel_id, req.note_id, novel_title=req.title, **update_kwargs)
-
- # Update mapping if title/summary changed
- mapping_update = {}
- if req.chapter_title:
- mapping_update["title"] = req.chapter_title
- if req.summary:
- mapping_update["summary"] = req.summary
-
- if mapping_update:
- project_manager.update_chapter_mapping(req.title, req.novel_id, req.note_id, mapping_update)
- return {"status": "success"}
- @app.delete("/chapter/delete")
- def delete_chapter(novel_id: str, title: str, note_id: str):
- chapter_agent.del_chapter(novel_id, note_id, novel_title=title)
-
- project_manager.remove_chapter_mapping(title, novel_id, note_id)
- return {"status": "success"}
- if __name__ == "__main__":
- uvicorn.run(app, host=os.getenv("HOST"), port=int(os.getenv("PORT")))
|