app.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. import sys
  2. import os
  3. import json
  4. import uvicorn
  5. from fastapi import FastAPI, HTTPException, Body
  6. from fastapi.middleware.cors import CORSMiddleware
  7. from pydantic import BaseModel
  8. from typing import List, Optional, Dict, Any
  9. # Add parent directory to sys.path to import agents
  10. sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
  11. # Add agents directory to sys.path so internal imports in agents work
  12. sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../agents")))
  13. from agents.outline_agent import OutlineAgent
  14. from agents.chapter_generate_agent import ChapterGenerateAgent
  15. from hello_agents import HelloAgentsLLM
  16. app = FastAPI()
  17. # Enable CORS for frontend
  18. app.add_middleware(
  19. CORSMiddleware,
  20. allow_origins=["*"], # Allows all origins
  21. allow_credentials=True,
  22. allow_methods=["*"], # Allows all methods
  23. allow_headers=["*"], # Allows all headers
  24. )
  25. # Data Models
  26. class OutlineRequest(BaseModel):
  27. novel_id: str
  28. title: str
  29. user_input: str
  30. tags: Optional[List[str]] = []
  31. target_length: Optional[int] = 3000
  32. style_tags: Dict[str, str] = {} # e.g. {"style": "dark", "tone": "serious"}
  33. class OutlineUpdateRequest(BaseModel):
  34. novel_id: str
  35. title: str
  36. note_id: str
  37. content: str
  38. tags: Optional[List[str]] = None
  39. class ChapterGenerateRequest(BaseModel):
  40. novel_id: str
  41. title: str
  42. user_input: str
  43. num_chapters: int = 1
  44. chapter_length: int = 3000
  45. class ChapterUpdateRequest(BaseModel):
  46. novel_id: str
  47. title: str
  48. note_id: str
  49. content: Optional[str] = None
  50. chapter_title: Optional[str] = None
  51. summary: Optional[str] = None
  52. next_chapter_prediction: Optional[str] = None
  53. # Manager
  54. class ProjectManager:
  55. def __init__(self, workspace="./outputs"):
  56. self.workspace = workspace
  57. if not os.path.exists(workspace):
  58. os.makedirs(workspace)
  59. def get_project_dir(self, title, novel_id):
  60. return os.path.join(self.workspace, f"{title}-{novel_id}")
  61. def get_mapping_file(self, title, novel_id):
  62. return os.path.join(self.get_project_dir(title, novel_id), "project_data.json")
  63. def load_mapping(self, title, novel_id):
  64. path = self.get_mapping_file(title, novel_id)
  65. if os.path.exists(path):
  66. with open(path, "r", encoding="utf-8") as f:
  67. return json.load(f)
  68. return {"novel_id": novel_id, "title": title, "outline_id": None, "chapters": []}
  69. def save_mapping(self, title, novel_id, data):
  70. path = self.get_mapping_file(title, novel_id)
  71. project_dir = self.get_project_dir(title, novel_id)
  72. if not os.path.exists(project_dir):
  73. os.makedirs(project_dir)
  74. with open(path, "w", encoding="utf-8") as f:
  75. json.dump(data, f, indent=2, ensure_ascii=False)
  76. def update_outline_mapping(self, title, novel_id, outline_id):
  77. data = self.load_mapping(title, novel_id)
  78. data["outline_id"] = outline_id
  79. self.save_mapping(title, novel_id, data)
  80. def add_chapter_mapping(self, title, novel_id, chapter_data):
  81. data = self.load_mapping(title, novel_id)
  82. data["chapters"].append(chapter_data)
  83. self.save_mapping(title, novel_id, data)
  84. def update_chapter_mapping(self, title, novel_id, note_id, update_data):
  85. data = self.load_mapping(title, novel_id)
  86. for chapter in data["chapters"]:
  87. if chapter["id"] == note_id:
  88. chapter.update(update_data)
  89. break
  90. self.save_mapping(title, novel_id, data)
  91. def remove_chapter_mapping(self, title, novel_id, note_id):
  92. data = self.load_mapping(title, novel_id)
  93. data["chapters"] = [c for c in data["chapters"] if c["id"] != note_id]
  94. self.save_mapping(title, novel_id, data)
  95. project_manager = ProjectManager()
  96. # Agents
  97. llm_instance = HelloAgentsLLM(model=os.getenv("LLM_MODEL_ID"))
  98. outline_agent = OutlineAgent(name="OutlineAgent", llm=llm_instance, workspace="./outputs")
  99. chapter_agent = ChapterGenerateAgent(
  100. name="ChapterAgent",
  101. llm=llm_instance,
  102. workspace="./outputs",
  103. chapter_length=3000 # Default length, can be overridden in run
  104. )
  105. # API Endpoints
  106. @app.get("/projects/{title}/{novel_id}")
  107. def get_project_data(title: str, novel_id: str):
  108. return project_manager.load_mapping(title, novel_id)
  109. # --- Outline ---
  110. @app.post("/outline/generate")
  111. def generate_outline(req: OutlineRequest):
  112. # Construct kwargs for run
  113. run_kwargs = {
  114. "novel_id": req.novel_id,
  115. "title": req.title,
  116. "target_length": req.target_length
  117. }
  118. run_kwargs.update(req.style_tags)
  119. response, note_id = outline_agent.run(req.user_input, **run_kwargs)
  120. project_manager.update_outline_mapping(req.title, req.novel_id, note_id)
  121. return {"note_id": note_id, "content": response}
  122. @app.get("/outline/{title}/{novel_id}/{note_id}")
  123. def get_outline(title: str, novel_id: str, note_id: str):
  124. content = outline_agent.get_outline(novel_id, note_id, title=title)
  125. # Remove frontmatter if present (simple check)
  126. # NoteTool returns raw content usually.
  127. # Frontmatter format: --- ... ---
  128. if content.startswith("---"):
  129. parts = content.split("---", 2)
  130. if len(parts) >= 3:
  131. content = parts[2].strip()
  132. return {"content": content}
  133. @app.put("/outline/update")
  134. def update_outline(req: OutlineUpdateRequest):
  135. outline_agent.update_outline(req.novel_id, req.note_id, title=req.title, content=req.content, tags=req.tags)
  136. return {"status": "success"}
  137. @app.delete("/outline/delete")
  138. def delete_outline(novel_id: str, title: str, note_id: str):
  139. outline_agent.del_outline(novel_id, note_id, title=title)
  140. data = project_manager.load_mapping(title, novel_id)
  141. if data["outline_id"] == note_id:
  142. data["outline_id"] = None
  143. project_manager.save_mapping(title, novel_id, data)
  144. return {"status": "success"}
  145. # --- Chapters ---
  146. @app.post("/chapter/generate")
  147. def generate_chapters(req: ChapterGenerateRequest):
  148. generated_chapters = []
  149. current_input = req.user_input
  150. for i in range(req.num_chapters):
  151. try:
  152. chapter_data, note_id = chapter_agent.run(
  153. user_input=current_input,
  154. novel_id=req.novel_id,
  155. novel_title=req.title,
  156. chapter_length=req.chapter_length
  157. )
  158. # Clear input for subsequent chapters to rely on context/prediction
  159. if i == 0:
  160. current_input = ""
  161. chapter_info = {
  162. "id": note_id,
  163. "title": chapter_data.get("title", "Unknown"),
  164. "summary": chapter_data.get("summary", "")
  165. }
  166. generated_chapters.append(chapter_info)
  167. project_manager.add_chapter_mapping(req.title, req.novel_id, chapter_info)
  168. except Exception as e:
  169. print(f"Error generating chapter {i+1}: {e}")
  170. # Stop generating if one fails? Or continue?
  171. # Probably stop and return what we have.
  172. break
  173. return {"generated_chapters": generated_chapters}
  174. @app.get("/chapter/{title}/{novel_id}/{note_id}")
  175. def get_chapter(title: str, novel_id: str, note_id: str):
  176. path = os.path.join("./outputs", f"{title}-{novel_id}", "chapters", f"{note_id}.md")
  177. if os.path.exists(path):
  178. with open(path, "r", encoding="utf-8") as f:
  179. content = f.read()
  180. # Remove frontmatter
  181. if content.startswith("---"):
  182. parts = content.split("---", 2)
  183. if len(parts) >= 3:
  184. content = parts[2].strip()
  185. return {"content": content}
  186. raise HTTPException(status_code=404, detail="Chapter not found")
  187. @app.put("/chapter/update")
  188. def update_chapter(req: ChapterUpdateRequest):
  189. update_kwargs = {}
  190. if req.content is not None:
  191. update_kwargs["content"] = req.content
  192. if req.chapter_title is not None:
  193. update_kwargs["title"] = req.chapter_title
  194. if req.summary is not None:
  195. update_kwargs["summary"] = req.summary
  196. if req.next_chapter_prediction is not None:
  197. update_kwargs["next_chapter_prediction"] = req.next_chapter_prediction
  198. chapter_agent.update_chapter(req.novel_id, req.note_id, novel_title=req.title, **update_kwargs)
  199. # Update mapping if title/summary changed
  200. mapping_update = {}
  201. if req.chapter_title:
  202. mapping_update["title"] = req.chapter_title
  203. if req.summary:
  204. mapping_update["summary"] = req.summary
  205. if mapping_update:
  206. project_manager.update_chapter_mapping(req.title, req.novel_id, req.note_id, mapping_update)
  207. return {"status": "success"}
  208. @app.delete("/chapter/delete")
  209. def delete_chapter(novel_id: str, title: str, note_id: str):
  210. chapter_agent.del_chapter(novel_id, note_id, novel_title=title)
  211. project_manager.remove_chapter_mapping(title, novel_id, note_id)
  212. return {"status": "success"}
  213. if __name__ == "__main__":
  214. uvicorn.run(app, host=os.getenv("HOST"), port=int(os.getenv("PORT")))