From 34ede1c08294d464fd5117f48010855c673930a0 Mon Sep 17 00:00:00 2001 From: rxdaozhang <896836861@qq.com> Date: Tue, 11 Mar 2025 01:23:16 +0800 Subject: [PATCH 01/13] =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=9A=E4=BD=BF?= =?UTF-8?q?=E7=94=A8PlanStepStatus=E6=9E=9A=E4=B8=BE=E6=9B=BF=E4=BB=A3?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/flow/base.py | 27 ++++++++++++++++++++++++ app/flow/planning.py | 49 ++++++++++++++++++-------------------------- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/app/flow/base.py b/app/flow/base.py index 13b3e17..841c9cc 100644 --- a/app/flow/base.py +++ b/app/flow/base.py @@ -60,3 +60,30 @@ class BaseFlow(BaseModel, ABC): @abstractmethod async def execute(self, input_text: str) -> str: """Execute the flow with given input""" + +class PlanStepStatus(str, Enum): + """Enum class defining possible statuses of a plan step""" + NOT_STARTED = "not_started" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + BLOCKED = "blocked" + + @classmethod + def get_all_statuses(cls) -> list[str]: + """Return a list of all possible step status values""" + return [status.value for status in cls] + + @classmethod + def get_active_statuses(cls) -> list[str]: + """Return a list of values representing active statuses (not started or in progress)""" + return [cls.NOT_STARTED.value, cls.IN_PROGRESS.value] + + @classmethod + def get_status_marks(cls) -> Dict[str, str]: + """Return a mapping of statuses to their marker symbols""" + return { + cls.COMPLETED.value: "[✓]", + cls.IN_PROGRESS.value: "[→]", + cls.BLOCKED.value: "[!]", + cls.NOT_STARTED.value: "[ ]" + } diff --git a/app/flow/planning.py b/app/flow/planning.py index b3df48a..aad172f 100644 --- a/app/flow/planning.py +++ b/app/flow/planning.py @@ -5,7 +5,7 @@ from typing import Dict, List, Optional, Union from pydantic import Field from app.agent.base import BaseAgent -from app.flow.base import BaseFlow +from app.flow.base import BaseFlow, PlanStepStatus from app.llm import LLM from app.logger import logger from app.schema import AgentState, Message @@ -183,11 +183,11 @@ class PlanningFlow(BaseFlow): # Find first non-completed step for i, step in enumerate(steps): if i >= len(step_statuses): - status = "not_started" + status = PlanStepStatus.NOT_STARTED.value else: status = step_statuses[i] - if status in ["not_started", "in_progress"]: + if status in PlanStepStatus.get_active_statuses(): # Extract step type/category if available step_info = {"text": step} @@ -204,17 +204,17 @@ class PlanningFlow(BaseFlow): command="mark_step", plan_id=self.active_plan_id, step_index=i, - step_status="in_progress", + step_status=PlanStepStatus.IN_PROGRESS.value, ) except Exception as e: logger.warning(f"Error marking step as in_progress: {e}") # Update step status directly if needed if i < len(step_statuses): - step_statuses[i] = "in_progress" + step_statuses[i] = PlanStepStatus.IN_PROGRESS.value else: while len(step_statuses) < i: - step_statuses.append("not_started") - step_statuses.append("in_progress") + step_statuses.append(PlanStepStatus.NOT_STARTED.value) + step_statuses.append(PlanStepStatus.IN_PROGRESS.value) plan_data["step_statuses"] = step_statuses @@ -266,7 +266,7 @@ class PlanningFlow(BaseFlow): command="mark_step", plan_id=self.active_plan_id, step_index=self.current_step_index, - step_status="completed", + step_status=PlanStepStatus.COMPLETED.value, ) logger.info( f"Marked step {self.current_step_index} as completed in plan {self.active_plan_id}" @@ -280,10 +280,10 @@ class PlanningFlow(BaseFlow): # Ensure the step_statuses list is long enough while len(step_statuses) <= self.current_step_index: - step_statuses.append("not_started") + step_statuses.append(PlanStepStatus.NOT_STARTED.value) # Update the status - step_statuses[self.current_step_index] = "completed" + step_statuses[self.current_step_index] = PlanStepStatus.COMPLETED.value plan_data["step_statuses"] = step_statuses async def _get_plan_text(self) -> str: @@ -311,23 +311,18 @@ class PlanningFlow(BaseFlow): # Ensure step_statuses and step_notes match the number of steps while len(step_statuses) < len(steps): - step_statuses.append("not_started") + step_statuses.append(PlanStepStatus.NOT_STARTED.value) while len(step_notes) < len(steps): step_notes.append("") # Count steps by status - status_counts = { - "completed": 0, - "in_progress": 0, - "blocked": 0, - "not_started": 0, - } + status_counts = {status: 0 for status in PlanStepStatus.get_all_statuses()} for status in step_statuses: if status in status_counts: status_counts[status] += 1 - completed = status_counts["completed"] + completed = status_counts[PlanStepStatus.COMPLETED.value] total = len(steps) progress = (completed / total) * 100 if total > 0 else 0 @@ -337,22 +332,18 @@ class PlanningFlow(BaseFlow): plan_text += ( f"Progress: {completed}/{total} steps completed ({progress:.1f}%)\n" ) - plan_text += f"Status: {status_counts['completed']} completed, {status_counts['in_progress']} in progress, " - plan_text += f"{status_counts['blocked']} blocked, {status_counts['not_started']} not started\n\n" + plan_text += f"Status: {status_counts[PlanStepStatus.COMPLETED.value]} completed, {status_counts[PlanStepStatus.IN_PROGRESS.value]} in progress, " + plan_text += f"{status_counts[PlanStepStatus.BLOCKED.value]} blocked, {status_counts[PlanStepStatus.NOT_STARTED.value]} not started\n\n" plan_text += "Steps:\n" + status_marks = PlanStepStatus.get_status_marks() + for i, (step, status, notes) in enumerate( zip(steps, step_statuses, step_notes) ): - if status == "completed": - status_mark = "[✓]" - elif status == "in_progress": - status_mark = "[→]" - elif status == "blocked": - status_mark = "[!]" - else: # not_started - status_mark = "[ ]" - + # Use status marks to indicate step status + status_mark = status_marks.get(status, status_marks[PlanStepStatus.NOT_STARTED.value]) + plan_text += f"{i}. {status_mark} {step}\n" if notes: plan_text += f" Notes: {notes}\n" From f67af3580a42a7f8d91fe993737d7cd71a3776ff Mon Sep 17 00:00:00 2001 From: rxdaozhang <896836861@qq.com> Date: Tue, 11 Mar 2025 02:07:04 +0800 Subject: [PATCH 02/13] fix multiline string SyntaxError --- app.py | 87 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/app.py b/app.py index baad1cf..0d251b7 100644 --- a/app.py +++ b/app.py @@ -23,6 +23,7 @@ app.add_middleware( allow_headers=["*"], ) + class Task(BaseModel): id: str prompt: str @@ -32,9 +33,10 @@ class Task(BaseModel): def model_dump(self, *args, **kwargs): data = super().model_dump(*args, **kwargs) - data['created_at'] = self.created_at.isoformat() + data["created_at"] = self.created_at.isoformat() return data + class TaskManager: def __init__(self): self.tasks = {} @@ -43,61 +45,55 @@ class TaskManager: def create_task(self, prompt: str) -> Task: task_id = str(uuid.uuid4()) task = Task( - id=task_id, - prompt=prompt, - created_at=datetime.now(), - status="pending" + id=task_id, prompt=prompt, created_at=datetime.now(), status="pending" ) self.tasks[task_id] = task self.queues[task_id] = asyncio.Queue() return task - async def update_task_step(self, task_id: str, step: int, result: str, step_type: str = "step"): + async def update_task_step( + self, task_id: str, step: int, result: str, step_type: str = "step" + ): if task_id in self.tasks: task = self.tasks[task_id] task.steps.append({"step": step, "result": result, "type": step_type}) - await self.queues[task_id].put({ - "type": step_type, - "step": step, - "result": result - }) - await self.queues[task_id].put({ - "type": "status", - "status": task.status, - "steps": task.steps - }) + await self.queues[task_id].put( + {"type": step_type, "step": step, "result": result} + ) + await self.queues[task_id].put( + {"type": "status", "status": task.status, "steps": task.steps} + ) async def complete_task(self, task_id: str): if task_id in self.tasks: task = self.tasks[task_id] task.status = "completed" - await self.queues[task_id].put({ - "type": "status", - "status": task.status, - "steps": task.steps - }) + await self.queues[task_id].put( + {"type": "status", "status": task.status, "steps": task.steps} + ) await self.queues[task_id].put({"type": "complete"}) async def fail_task(self, task_id: str, error: str): if task_id in self.tasks: self.tasks[task_id].status = f"failed: {error}" - await self.queues[task_id].put({ - "type": "error", - "message": error - }) + await self.queues[task_id].put({"type": "error", "message": error}) + task_manager = TaskManager() + @app.get("/", response_class=HTMLResponse) async def index(request: Request): return templates.TemplateResponse("index.html", {"request": request}) + @app.post("/tasks") async def create_task(prompt: str = Body(..., embed=True)): task = task_manager.create_task(prompt) asyncio.create_task(run_task(task.id, prompt)) return {"task_id": task.id} + from app.agent.manus import Manus @@ -108,17 +104,21 @@ async def run_task(task_id: str, prompt: str): agent = Manus( name="Manus", description="A versatile agent that can solve various tasks using multiple tools", - max_steps=30 + max_steps=30, ) async def on_think(thought): await task_manager.update_task_step(task_id, 0, thought, "think") async def on_tool_execute(tool, input): - await task_manager.update_task_step(task_id, 0, f"Executing tool: {tool}\nInput: {input}", "tool") + await task_manager.update_task_step( + task_id, 0, f"Executing tool: {tool}\nInput: {input}", "tool" + ) async def on_action(action): - await task_manager.update_task_step(task_id, 0, f"Executing action: {action}", "act") + await task_manager.update_task_step( + task_id, 0, f"Executing action: {action}", "act" + ) async def on_run(step, result): await task_manager.update_task_step(task_id, step, result, "run") @@ -133,7 +133,7 @@ async def run_task(task_id: str, prompt: str): import re # 提取 - 后面的内容 - cleaned_message = re.sub(r'^.*? - ', '', message) + cleaned_message = re.sub(r"^.*? - ", "", message) event_type = "log" if "✨ Manus's thoughts:" in cleaned_message: @@ -147,7 +147,9 @@ async def run_task(task_id: str, prompt: str): elif "🏁 Special tool" in cleaned_message: event_type = "complete" - await task_manager.update_task_step(self.task_id, 0, cleaned_message, event_type) + await task_manager.update_task_step( + self.task_id, 0, cleaned_message, event_type + ) sse_handler = SSELogHandler(task_id) logger.add(sse_handler) @@ -158,6 +160,7 @@ async def run_task(task_id: str, prompt: str): except Exception as e: await task_manager.fail_task(task_id, str(e)) + @app.get("/tasks/{task_id}/events") async def task_events(task_id: str): async def event_generator(): @@ -169,11 +172,7 @@ async def task_events(task_id: str): task = task_manager.tasks.get(task_id) if task: - yield f"event: status\ndata: {dumps({ - 'type': 'status', - 'status': task.status, - 'steps': task.steps - })}\n\n" + yield f"event: status\ndata: {dumps({'type': 'status','status': task.status, 'steps': task.steps})}\n\n" while True: try: @@ -216,35 +215,37 @@ async def task_events(task_id: str): headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", - "X-Accel-Buffering": "no" - } + "X-Accel-Buffering": "no", + }, ) + @app.get("/tasks") async def get_tasks(): sorted_tasks = sorted( - task_manager.tasks.values(), - key=lambda task: task.created_at, - reverse=True + task_manager.tasks.values(), key=lambda task: task.created_at, reverse=True ) return JSONResponse( content=[task.model_dump() for task in sorted_tasks], - headers={"Content-Type": "application/json"} + headers={"Content-Type": "application/json"}, ) + @app.get("/tasks/{task_id}") async def get_task(task_id: str): if task_id not in task_manager.tasks: raise HTTPException(status_code=404, detail="Task not found") return task_manager.tasks[task_id] + @app.exception_handler(Exception) async def generic_exception_handler(request: Request, exc: Exception): return JSONResponse( - status_code=500, - content={"message": f"Server error: {str(exc)}"} + status_code=500, content={"message": f"Server error: {str(exc)}"} ) + if __name__ == "__main__": import uvicorn + uvicorn.run(app, host="localhost", port=5172) From 69325c701bc110e23dfc4e6718bd2a6dccae3797 Mon Sep 17 00:00:00 2001 From: rxdaozhang <896836861@qq.com> Date: Tue, 11 Mar 2025 02:14:15 +0800 Subject: [PATCH 03/13] fix multiline string SyntaxError another --- app.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app.py b/app.py index baad1cf..77ddac8 100644 --- a/app.py +++ b/app.py @@ -169,11 +169,7 @@ async def task_events(task_id: str): task = task_manager.tasks.get(task_id) if task: - yield f"event: status\ndata: {dumps({ - 'type': 'status', - 'status': task.status, - 'steps': task.steps - })}\n\n" + yield f"event: status\ndata: {dumps({'type': 'status','status': task.status,'steps': task.steps})}\n\n" while True: try: @@ -191,11 +187,7 @@ async def task_events(task_id: str): elif event["type"] == "step": task = task_manager.tasks.get(task_id) if task: - yield f"event: status\ndata: {dumps({ - 'type': 'status', - 'status': task.status, - 'steps': task.steps - })}\n\n" + yield f"event: status\ndata: {dumps({ 'type': 'status','status': task.status,'steps': task.steps })}\n\n" yield f"event: {event['type']}\ndata: {formatted_event}\n\n" elif event["type"] in ["think", "tool", "act", "run"]: yield f"event: {event['type']}\ndata: {formatted_event}\n\n" From 41308f3acac33b4315325f165b1e0b5221e99cd0 Mon Sep 17 00:00:00 2001 From: rxdaozhang <896836861@qq.com> Date: Tue, 11 Mar 2025 02:18:40 +0800 Subject: [PATCH 04/13] fix pre commit hook bug --- app.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index 5adb62e..cebca55 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,7 @@ import asyncio +import threading import uuid +import webbrowser from datetime import datetime from json import dumps @@ -10,6 +12,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel + app = FastAPI() app.mount("/static", StaticFiles(directory="static"), name="static") @@ -172,7 +175,9 @@ async def task_events(task_id: str): task = task_manager.tasks.get(task_id) if task: - yield f"event: status\ndata: {dumps({'type': 'status','status': task.status,'steps': task.steps})}\n\n" + message = {"type": "status", "status": task.status, "steps": task.steps} + json_message = dumps(message) + yield f"event: status\ndata: {json_message}\n\n" while True: try: @@ -190,7 +195,13 @@ async def task_events(task_id: str): elif event["type"] == "step": task = task_manager.tasks.get(task_id) if task: - yield f"event: status\ndata: {dumps({ 'type': 'status','status': task.status,'steps': task.steps })}\n\n" + message = { + "type": "status", + "status": task.status, + "steps": task.steps, + } + json_message = dumps(message) + yield f"event: status\ndata: {json_message}\n\n" yield f"event: {event['type']}\ndata: {formatted_event}\n\n" elif event["type"] in ["think", "tool", "act", "run"]: yield f"event: {event['type']}\ndata: {formatted_event}\n\n" @@ -241,7 +252,12 @@ async def generic_exception_handler(request: Request, exc: Exception): ) +def open_local_browser(): + webbrowser.open_new_tab("http://localhost:5172") + + if __name__ == "__main__": + threading.Timer(3, open_local_browser).start() import uvicorn - uvicorn.run(app, host="localhost", port=5172) + uvicorn.run(app, host="localhost", port=5172) \ No newline at end of file From 2fcad5f0972fc617482fac391f2ff2beeac0bfd6 Mon Sep 17 00:00:00 2001 From: rxdaozhang <896836861@qq.com> Date: Tue, 11 Mar 2025 02:20:50 +0800 Subject: [PATCH 05/13] try fix pre commit hook bug in app.py --- app.py | 109 +++++++++++++++++++++++++-------------------------------- 1 file changed, 48 insertions(+), 61 deletions(-) diff --git a/app.py b/app.py index cebca55..93aee5f 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,5 @@ import asyncio -import threading import uuid -import webbrowser from datetime import datetime from json import dumps @@ -12,7 +10,6 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel - app = FastAPI() app.mount("/static", StaticFiles(directory="static"), name="static") @@ -26,7 +23,6 @@ app.add_middleware( allow_headers=["*"], ) - class Task(BaseModel): id: str prompt: str @@ -36,10 +32,9 @@ class Task(BaseModel): def model_dump(self, *args, **kwargs): data = super().model_dump(*args, **kwargs) - data["created_at"] = self.created_at.isoformat() + data['created_at'] = self.created_at.isoformat() return data - class TaskManager: def __init__(self): self.tasks = {} @@ -48,55 +43,61 @@ class TaskManager: def create_task(self, prompt: str) -> Task: task_id = str(uuid.uuid4()) task = Task( - id=task_id, prompt=prompt, created_at=datetime.now(), status="pending" + id=task_id, + prompt=prompt, + created_at=datetime.now(), + status="pending" ) self.tasks[task_id] = task self.queues[task_id] = asyncio.Queue() return task - async def update_task_step( - self, task_id: str, step: int, result: str, step_type: str = "step" - ): + async def update_task_step(self, task_id: str, step: int, result: str, step_type: str = "step"): if task_id in self.tasks: task = self.tasks[task_id] task.steps.append({"step": step, "result": result, "type": step_type}) - await self.queues[task_id].put( - {"type": step_type, "step": step, "result": result} - ) - await self.queues[task_id].put( - {"type": "status", "status": task.status, "steps": task.steps} - ) + await self.queues[task_id].put({ + "type": step_type, + "step": step, + "result": result + }) + await self.queues[task_id].put({ + "type": "status", + "status": task.status, + "steps": task.steps + }) async def complete_task(self, task_id: str): if task_id in self.tasks: task = self.tasks[task_id] task.status = "completed" - await self.queues[task_id].put( - {"type": "status", "status": task.status, "steps": task.steps} - ) + await self.queues[task_id].put({ + "type": "status", + "status": task.status, + "steps": task.steps + }) await self.queues[task_id].put({"type": "complete"}) async def fail_task(self, task_id: str, error: str): if task_id in self.tasks: self.tasks[task_id].status = f"failed: {error}" - await self.queues[task_id].put({"type": "error", "message": error}) - + await self.queues[task_id].put({ + "type": "error", + "message": error + }) task_manager = TaskManager() - @app.get("/", response_class=HTMLResponse) async def index(request: Request): return templates.TemplateResponse("index.html", {"request": request}) - @app.post("/tasks") async def create_task(prompt: str = Body(..., embed=True)): task = task_manager.create_task(prompt) asyncio.create_task(run_task(task.id, prompt)) return {"task_id": task.id} - from app.agent.manus import Manus @@ -107,21 +108,17 @@ async def run_task(task_id: str, prompt: str): agent = Manus( name="Manus", description="A versatile agent that can solve various tasks using multiple tools", - max_steps=30, + max_steps=30 ) async def on_think(thought): await task_manager.update_task_step(task_id, 0, thought, "think") async def on_tool_execute(tool, input): - await task_manager.update_task_step( - task_id, 0, f"Executing tool: {tool}\nInput: {input}", "tool" - ) + await task_manager.update_task_step(task_id, 0, f"Executing tool: {tool}\nInput: {input}", "tool") async def on_action(action): - await task_manager.update_task_step( - task_id, 0, f"Executing action: {action}", "act" - ) + await task_manager.update_task_step(task_id, 0, f"Executing action: {action}", "act") async def on_run(step, result): await task_manager.update_task_step(task_id, step, result, "run") @@ -136,7 +133,7 @@ async def run_task(task_id: str, prompt: str): import re # 提取 - 后面的内容 - cleaned_message = re.sub(r"^.*? - ", "", message) + cleaned_message = re.sub(r'^.*? - ', '', message) event_type = "log" if "✨ Manus's thoughts:" in cleaned_message: @@ -150,9 +147,7 @@ async def run_task(task_id: str, prompt: str): elif "🏁 Special tool" in cleaned_message: event_type = "complete" - await task_manager.update_task_step( - self.task_id, 0, cleaned_message, event_type - ) + await task_manager.update_task_step(self.task_id, 0, cleaned_message, event_type) sse_handler = SSELogHandler(task_id) logger.add(sse_handler) @@ -163,7 +158,6 @@ async def run_task(task_id: str, prompt: str): except Exception as e: await task_manager.fail_task(task_id, str(e)) - @app.get("/tasks/{task_id}/events") async def task_events(task_id: str): async def event_generator(): @@ -175,9 +169,11 @@ async def task_events(task_id: str): task = task_manager.tasks.get(task_id) if task: - message = {"type": "status", "status": task.status, "steps": task.steps} - json_message = dumps(message) - yield f"event: status\ndata: {json_message}\n\n" + yield f"event: status\ndata: {dumps({ + 'type': 'status', + 'status': task.status, + 'steps': task.steps + })}\n\n" while True: try: @@ -195,13 +191,11 @@ async def task_events(task_id: str): elif event["type"] == "step": task = task_manager.tasks.get(task_id) if task: - message = { - "type": "status", - "status": task.status, - "steps": task.steps, - } - json_message = dumps(message) - yield f"event: status\ndata: {json_message}\n\n" + yield f"event: status\ndata: {dumps({ + 'type': 'status', + 'status': task.status, + 'steps': task.steps + })}\n\n" yield f"event: {event['type']}\ndata: {formatted_event}\n\n" elif event["type"] in ["think", "tool", "act", "run"]: yield f"event: {event['type']}\ndata: {formatted_event}\n\n" @@ -222,42 +216,35 @@ async def task_events(task_id: str): headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", - "X-Accel-Buffering": "no", - }, + "X-Accel-Buffering": "no" + } ) - @app.get("/tasks") async def get_tasks(): sorted_tasks = sorted( - task_manager.tasks.values(), key=lambda task: task.created_at, reverse=True + task_manager.tasks.values(), + key=lambda task: task.created_at, + reverse=True ) return JSONResponse( content=[task.model_dump() for task in sorted_tasks], - headers={"Content-Type": "application/json"}, + headers={"Content-Type": "application/json"} ) - @app.get("/tasks/{task_id}") async def get_task(task_id: str): if task_id not in task_manager.tasks: raise HTTPException(status_code=404, detail="Task not found") return task_manager.tasks[task_id] - @app.exception_handler(Exception) async def generic_exception_handler(request: Request, exc: Exception): return JSONResponse( - status_code=500, content={"message": f"Server error: {str(exc)}"} + status_code=500, + content={"message": f"Server error: {str(exc)}"} ) - -def open_local_browser(): - webbrowser.open_new_tab("http://localhost:5172") - - if __name__ == "__main__": - threading.Timer(3, open_local_browser).start() import uvicorn - uvicorn.run(app, host="localhost", port=5172) \ No newline at end of file From a256d5589a087acfac12b2f6a5446c9b1f2badaa Mon Sep 17 00:00:00 2001 From: rxdaozhang <896836861@qq.com> Date: Tue, 11 Mar 2025 02:27:24 +0800 Subject: [PATCH 06/13] Revert "try fix pre commit hook bug in app.py" This reverts commit 2fcad5f0972fc617482fac391f2ff2beeac0bfd6. --- app.py | 109 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 61 insertions(+), 48 deletions(-) diff --git a/app.py b/app.py index 93aee5f..cebca55 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,7 @@ import asyncio +import threading import uuid +import webbrowser from datetime import datetime from json import dumps @@ -10,6 +12,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel + app = FastAPI() app.mount("/static", StaticFiles(directory="static"), name="static") @@ -23,6 +26,7 @@ app.add_middleware( allow_headers=["*"], ) + class Task(BaseModel): id: str prompt: str @@ -32,9 +36,10 @@ class Task(BaseModel): def model_dump(self, *args, **kwargs): data = super().model_dump(*args, **kwargs) - data['created_at'] = self.created_at.isoformat() + data["created_at"] = self.created_at.isoformat() return data + class TaskManager: def __init__(self): self.tasks = {} @@ -43,61 +48,55 @@ class TaskManager: def create_task(self, prompt: str) -> Task: task_id = str(uuid.uuid4()) task = Task( - id=task_id, - prompt=prompt, - created_at=datetime.now(), - status="pending" + id=task_id, prompt=prompt, created_at=datetime.now(), status="pending" ) self.tasks[task_id] = task self.queues[task_id] = asyncio.Queue() return task - async def update_task_step(self, task_id: str, step: int, result: str, step_type: str = "step"): + async def update_task_step( + self, task_id: str, step: int, result: str, step_type: str = "step" + ): if task_id in self.tasks: task = self.tasks[task_id] task.steps.append({"step": step, "result": result, "type": step_type}) - await self.queues[task_id].put({ - "type": step_type, - "step": step, - "result": result - }) - await self.queues[task_id].put({ - "type": "status", - "status": task.status, - "steps": task.steps - }) + await self.queues[task_id].put( + {"type": step_type, "step": step, "result": result} + ) + await self.queues[task_id].put( + {"type": "status", "status": task.status, "steps": task.steps} + ) async def complete_task(self, task_id: str): if task_id in self.tasks: task = self.tasks[task_id] task.status = "completed" - await self.queues[task_id].put({ - "type": "status", - "status": task.status, - "steps": task.steps - }) + await self.queues[task_id].put( + {"type": "status", "status": task.status, "steps": task.steps} + ) await self.queues[task_id].put({"type": "complete"}) async def fail_task(self, task_id: str, error: str): if task_id in self.tasks: self.tasks[task_id].status = f"failed: {error}" - await self.queues[task_id].put({ - "type": "error", - "message": error - }) + await self.queues[task_id].put({"type": "error", "message": error}) + task_manager = TaskManager() + @app.get("/", response_class=HTMLResponse) async def index(request: Request): return templates.TemplateResponse("index.html", {"request": request}) + @app.post("/tasks") async def create_task(prompt: str = Body(..., embed=True)): task = task_manager.create_task(prompt) asyncio.create_task(run_task(task.id, prompt)) return {"task_id": task.id} + from app.agent.manus import Manus @@ -108,17 +107,21 @@ async def run_task(task_id: str, prompt: str): agent = Manus( name="Manus", description="A versatile agent that can solve various tasks using multiple tools", - max_steps=30 + max_steps=30, ) async def on_think(thought): await task_manager.update_task_step(task_id, 0, thought, "think") async def on_tool_execute(tool, input): - await task_manager.update_task_step(task_id, 0, f"Executing tool: {tool}\nInput: {input}", "tool") + await task_manager.update_task_step( + task_id, 0, f"Executing tool: {tool}\nInput: {input}", "tool" + ) async def on_action(action): - await task_manager.update_task_step(task_id, 0, f"Executing action: {action}", "act") + await task_manager.update_task_step( + task_id, 0, f"Executing action: {action}", "act" + ) async def on_run(step, result): await task_manager.update_task_step(task_id, step, result, "run") @@ -133,7 +136,7 @@ async def run_task(task_id: str, prompt: str): import re # 提取 - 后面的内容 - cleaned_message = re.sub(r'^.*? - ', '', message) + cleaned_message = re.sub(r"^.*? - ", "", message) event_type = "log" if "✨ Manus's thoughts:" in cleaned_message: @@ -147,7 +150,9 @@ async def run_task(task_id: str, prompt: str): elif "🏁 Special tool" in cleaned_message: event_type = "complete" - await task_manager.update_task_step(self.task_id, 0, cleaned_message, event_type) + await task_manager.update_task_step( + self.task_id, 0, cleaned_message, event_type + ) sse_handler = SSELogHandler(task_id) logger.add(sse_handler) @@ -158,6 +163,7 @@ async def run_task(task_id: str, prompt: str): except Exception as e: await task_manager.fail_task(task_id, str(e)) + @app.get("/tasks/{task_id}/events") async def task_events(task_id: str): async def event_generator(): @@ -169,11 +175,9 @@ async def task_events(task_id: str): task = task_manager.tasks.get(task_id) if task: - yield f"event: status\ndata: {dumps({ - 'type': 'status', - 'status': task.status, - 'steps': task.steps - })}\n\n" + message = {"type": "status", "status": task.status, "steps": task.steps} + json_message = dumps(message) + yield f"event: status\ndata: {json_message}\n\n" while True: try: @@ -191,11 +195,13 @@ async def task_events(task_id: str): elif event["type"] == "step": task = task_manager.tasks.get(task_id) if task: - yield f"event: status\ndata: {dumps({ - 'type': 'status', - 'status': task.status, - 'steps': task.steps - })}\n\n" + message = { + "type": "status", + "status": task.status, + "steps": task.steps, + } + json_message = dumps(message) + yield f"event: status\ndata: {json_message}\n\n" yield f"event: {event['type']}\ndata: {formatted_event}\n\n" elif event["type"] in ["think", "tool", "act", "run"]: yield f"event: {event['type']}\ndata: {formatted_event}\n\n" @@ -216,35 +222,42 @@ async def task_events(task_id: str): headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", - "X-Accel-Buffering": "no" - } + "X-Accel-Buffering": "no", + }, ) + @app.get("/tasks") async def get_tasks(): sorted_tasks = sorted( - task_manager.tasks.values(), - key=lambda task: task.created_at, - reverse=True + task_manager.tasks.values(), key=lambda task: task.created_at, reverse=True ) return JSONResponse( content=[task.model_dump() for task in sorted_tasks], - headers={"Content-Type": "application/json"} + headers={"Content-Type": "application/json"}, ) + @app.get("/tasks/{task_id}") async def get_task(task_id: str): if task_id not in task_manager.tasks: raise HTTPException(status_code=404, detail="Task not found") return task_manager.tasks[task_id] + @app.exception_handler(Exception) async def generic_exception_handler(request: Request, exc: Exception): return JSONResponse( - status_code=500, - content={"message": f"Server error: {str(exc)}"} + status_code=500, content={"message": f"Server error: {str(exc)}"} ) + +def open_local_browser(): + webbrowser.open_new_tab("http://localhost:5172") + + if __name__ == "__main__": + threading.Timer(3, open_local_browser).start() import uvicorn + uvicorn.run(app, host="localhost", port=5172) \ No newline at end of file From c37c6c19b5c58046918a58d41bf9c540579f6b7f Mon Sep 17 00:00:00 2001 From: rxdaozhang <896836861@qq.com> Date: Tue, 11 Mar 2025 02:28:00 +0800 Subject: [PATCH 07/13] Revert "fix pre commit hook bug" This reverts commit 41308f3acac33b4315325f165b1e0b5221e99cd0. --- app.py | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/app.py b/app.py index cebca55..5adb62e 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,5 @@ import asyncio -import threading import uuid -import webbrowser from datetime import datetime from json import dumps @@ -12,7 +10,6 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel - app = FastAPI() app.mount("/static", StaticFiles(directory="static"), name="static") @@ -175,9 +172,7 @@ async def task_events(task_id: str): task = task_manager.tasks.get(task_id) if task: - message = {"type": "status", "status": task.status, "steps": task.steps} - json_message = dumps(message) - yield f"event: status\ndata: {json_message}\n\n" + yield f"event: status\ndata: {dumps({'type': 'status','status': task.status,'steps': task.steps})}\n\n" while True: try: @@ -195,13 +190,7 @@ async def task_events(task_id: str): elif event["type"] == "step": task = task_manager.tasks.get(task_id) if task: - message = { - "type": "status", - "status": task.status, - "steps": task.steps, - } - json_message = dumps(message) - yield f"event: status\ndata: {json_message}\n\n" + yield f"event: status\ndata: {dumps({ 'type': 'status','status': task.status,'steps': task.steps })}\n\n" yield f"event: {event['type']}\ndata: {formatted_event}\n\n" elif event["type"] in ["think", "tool", "act", "run"]: yield f"event: {event['type']}\ndata: {formatted_event}\n\n" @@ -252,12 +241,7 @@ async def generic_exception_handler(request: Request, exc: Exception): ) -def open_local_browser(): - webbrowser.open_new_tab("http://localhost:5172") - - if __name__ == "__main__": - threading.Timer(3, open_local_browser).start() import uvicorn - uvicorn.run(app, host="localhost", port=5172) \ No newline at end of file + uvicorn.run(app, host="localhost", port=5172) From d9418c16685c35da873e8392879088a41c071434 Mon Sep 17 00:00:00 2001 From: rxdaozhang <896836861@qq.com> Date: Tue, 11 Mar 2025 02:36:50 +0800 Subject: [PATCH 08/13] last try --- app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index 5adb62e..79bd0f6 100644 --- a/app.py +++ b/app.py @@ -172,7 +172,7 @@ async def task_events(task_id: str): task = task_manager.tasks.get(task_id) if task: - yield f"event: status\ndata: {dumps({'type': 'status','status': task.status,'steps': task.steps})}\n\n" + yield f"event: status\ndata: {dumps({'type': 'status', 'status': task.status, 'steps': task.steps})}\n\n" while True: try: @@ -190,7 +190,7 @@ async def task_events(task_id: str): elif event["type"] == "step": task = task_manager.tasks.get(task_id) if task: - yield f"event: status\ndata: {dumps({ 'type': 'status','status': task.status,'steps': task.steps })}\n\n" + yield f"event: status\ndata: {dumps({'type': 'status', 'status': task.status, 'steps': task.steps})}\n\n" yield f"event: {event['type']}\ndata: {formatted_event}\n\n" elif event["type"] in ["think", "tool", "act", "run"]: yield f"event: {event['type']}\ndata: {formatted_event}\n\n" @@ -244,4 +244,4 @@ async def generic_exception_handler(request: Request, exc: Exception): if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="localhost", port=5172) + uvicorn.run(app, host="localhost", port=5172) \ No newline at end of file From 229d6706a493c4ce0f23446c673d95a21a72f881 Mon Sep 17 00:00:00 2001 From: liangxinbing <1580466765@qq.com> Date: Tue, 11 Mar 2025 07:31:18 +0800 Subject: [PATCH 09/13] format code --- README.md | 6 +----- README_zh.md | 4 ---- app.py | 2 +- app/flow/base.py | 10 ++++++---- app/flow/planning.py | 8 +++++--- 5 files changed, 13 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index e035313..5075ae2 100644 --- a/README.md +++ b/README.md @@ -130,10 +130,6 @@ We welcome any friendly suggestions and helpful contributions! Just create issue Or contact @mannaandpoem via 📧email: mannaandpoem@gmail.com -## Roadmap - -- [ ] Improve the UI’s visual appeal to create a more intuitive and seamless user experience. - ## Community Group Join our networking group on Feishu and share your experience with other developers! @@ -162,4 +158,4 @@ OpenManus is built by contributors from MetaGPT. Huge thanks to this agent commu journal = {GitHub repository}, howpublished = {\url{https://github.com/mannaandpoem/OpenManus}}, } -``` \ No newline at end of file +``` diff --git a/README_zh.md b/README_zh.md index fb50333..fef68c1 100644 --- a/README_zh.md +++ b/README_zh.md @@ -130,10 +130,6 @@ python run_flow.py 或通过 📧 邮件联系 @mannaandpoem:mannaandpoem@gmail.com -## 发展路线 - -- [ ] 提高用户界面的视觉吸引力,以创建更直观和无缝的用户体验。 - ## 交流群 加入我们的飞书交流群,与其他开发者分享经验! diff --git a/app.py b/app.py index 79bd0f6..b827484 100644 --- a/app.py +++ b/app.py @@ -244,4 +244,4 @@ async def generic_exception_handler(request: Request, exc: Exception): if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="localhost", port=5172) \ No newline at end of file + uvicorn.run(app, host="localhost", port=5172) diff --git a/app/flow/base.py b/app/flow/base.py index 841c9cc..13066ce 100644 --- a/app/flow/base.py +++ b/app/flow/base.py @@ -61,23 +61,25 @@ class BaseFlow(BaseModel, ABC): async def execute(self, input_text: str) -> str: """Execute the flow with given input""" + class PlanStepStatus(str, Enum): """Enum class defining possible statuses of a plan step""" + NOT_STARTED = "not_started" IN_PROGRESS = "in_progress" COMPLETED = "completed" BLOCKED = "blocked" - + @classmethod def get_all_statuses(cls) -> list[str]: """Return a list of all possible step status values""" return [status.value for status in cls] - + @classmethod def get_active_statuses(cls) -> list[str]: """Return a list of values representing active statuses (not started or in progress)""" return [cls.NOT_STARTED.value, cls.IN_PROGRESS.value] - + @classmethod def get_status_marks(cls) -> Dict[str, str]: """Return a mapping of statuses to their marker symbols""" @@ -85,5 +87,5 @@ class PlanStepStatus(str, Enum): cls.COMPLETED.value: "[✓]", cls.IN_PROGRESS.value: "[→]", cls.BLOCKED.value: "[!]", - cls.NOT_STARTED.value: "[ ]" + cls.NOT_STARTED.value: "[ ]", } diff --git a/app/flow/planning.py b/app/flow/planning.py index aad172f..808949f 100644 --- a/app/flow/planning.py +++ b/app/flow/planning.py @@ -337,13 +337,15 @@ class PlanningFlow(BaseFlow): plan_text += "Steps:\n" status_marks = PlanStepStatus.get_status_marks() - + for i, (step, status, notes) in enumerate( zip(steps, step_statuses, step_notes) ): # Use status marks to indicate step status - status_mark = status_marks.get(status, status_marks[PlanStepStatus.NOT_STARTED.value]) - + status_mark = status_marks.get( + status, status_marks[PlanStepStatus.NOT_STARTED.value] + ) + plan_text += f"{i}. {status_mark} {step}\n" if notes: plan_text += f" Notes: {notes}\n" From 8018124af24ef21408573daa72078c01de80cd15 Mon Sep 17 00:00:00 2001 From: Feige-cn Date: Tue, 11 Mar 2025 08:39:16 +0800 Subject: [PATCH 10/13] modified by hook --- app.py | 8 + static/main.js | 605 +++++++++-------------------------------------- static/style.css | 35 ++- 3 files changed, 142 insertions(+), 506 deletions(-) diff --git a/app.py b/app.py index b827484..2e1959a 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,7 @@ import asyncio +import threading import uuid +import webbrowser from datetime import datetime from json import dumps @@ -10,6 +12,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel + app = FastAPI() app.mount("/static", StaticFiles(directory="static"), name="static") @@ -241,7 +244,12 @@ async def generic_exception_handler(request: Request, exc: Exception): ) +def open_local_browser(): + webbrowser.open_new_tab("http://localhost:5172") + + if __name__ == "__main__": import uvicorn + threading.Timer(3, open_local_browser).start() uvicorn.run(app, host="localhost", port=5172) diff --git a/static/main.js b/static/main.js index 49d5dd7..c219d20 100644 --- a/static/main.js +++ b/static/main.js @@ -50,12 +50,12 @@ function setupSSE(taskId) { const maxRetries = 3; const retryDelay = 2000; + const container = document.getElementById('task-container'); + function connect() { const eventSource = new EventSource(`/tasks/${taskId}/events`); currentEventSource = eventSource; - const container = document.getElementById('task-container'); - let heartbeatTimer = setInterval(() => { container.innerHTML += '
·
'; }, 5000); @@ -71,94 +71,20 @@ function setupSSE(taskId) { }); }, 10000); - if (!eventSource._listenersAdded) { - eventSource._listenersAdded = true; - - let lastResultContent = ''; - eventSource.addEventListener('status', (event) => { + const handleEvent = (event, type) => { clearInterval(heartbeatTimer); try { const data = JSON.parse(event.data); container.querySelector('.loading')?.remove(); container.classList.add('active'); - const welcomeMessage = document.querySelector('.welcome-message'); - if (welcomeMessage) { - welcomeMessage.style.display = 'none'; - } - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - // Save result content - if (data.steps && data.steps.length > 0) { - // Iterate through all steps, find the last result type - for (let i = data.steps.length - 1; i >= 0; i--) { - if (data.steps[i].type === 'result') { - lastResultContent = data.steps[i].result; - break; - } - } - } - - // Parse and display each step with proper formatting - stepContainer.innerHTML = data.steps.map(step => { - const content = step.result; - const timestamp = new Date().toLocaleTimeString(); - return ` -
-
- ${getEventIcon(step.type)} [${timestamp}] ${getEventLabel(step.type)}: -
${content}
-
-
- `; - }).join(''); - - // Auto-scroll to bottom - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); - } catch (e) { - console.error('Status update failed:', e); - } - }); - - // Add handler for think event - eventSource.addEventListener('think', (event) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - const content = data.result; - const timestamp = new Date().toLocaleTimeString(); - - const step = document.createElement('div'); - step.className = 'step-item think'; - step.innerHTML = ` -
- ${getEventIcon('think')} [${timestamp}] ${getEventLabel('think')}: -
${content}
-
- `; + const stepContainer = ensureStepContainer(container); + const { formattedContent, timestamp } = formatStepContent(data, type); + const step = createStepElement(type, formattedContent, timestamp); stepContainer.appendChild(step); - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); + autoScroll(stepContainer); - // Update task status fetch(`/tasks/${taskId}`) .then(response => response.json()) .then(task => { @@ -168,236 +94,16 @@ function setupSSE(taskId) { console.error('Status update failed:', error); }); } catch (e) { - console.error('Think event handling failed:', e); + console.error(`Error handling ${type} event:`, e); } + }; + + const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message']; + eventTypes.forEach(type => { + eventSource.addEventListener(type, (event) => handleEvent(event, type)); }); - // Add handler for tool event - eventSource.addEventListener('tool', (event) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - const content = data.result; - const timestamp = new Date().toLocaleTimeString(); - - const step = document.createElement('div'); - step.className = 'step-item tool'; - step.innerHTML = ` -
- ${getEventIcon('tool')} [${timestamp}] ${getEventLabel('tool')}: -
${content}
-
- `; - - stepContainer.appendChild(step); - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); - - // Update task status - fetch(`/tasks/${taskId}`) - .then(response => response.json()) - .then(task => { - updateTaskStatus(task); - }) - .catch(error => { - console.error('Status update failed:', error); - }); - } catch (e) { - console.error('Tool event handling failed:', e); - } - }); - - // Add handler for act event - eventSource.addEventListener('act', (event) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - const content = data.result; - const timestamp = new Date().toLocaleTimeString(); - - const step = document.createElement('div'); - step.className = 'step-item act'; - step.innerHTML = ` -
- ${getEventIcon('act')} [${timestamp}] ${getEventLabel('act')}: -
${content}
-
- `; - - stepContainer.appendChild(step); - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); - - // Update task status - fetch(`/tasks/${taskId}`) - .then(response => response.json()) - .then(task => { - updateTaskStatus(task); - }) - .catch(error => { - console.error('Status update failed:', error); - }); - } catch (e) { - console.error('Act event handling failed:', e); - } - }); - - // Add handler for log event - eventSource.addEventListener('log', (event) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - const content = data.result; - const timestamp = new Date().toLocaleTimeString(); - - const step = document.createElement('div'); - step.className = 'step-item log'; - step.innerHTML = ` -
- ${getEventIcon('log')} [${timestamp}] ${getEventLabel('log')}: -
${content}
-
- `; - - stepContainer.appendChild(step); - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); - - // Update task status - fetch(`/tasks/${taskId}`) - .then(response => response.json()) - .then(task => { - updateTaskStatus(task); - }) - .catch(error => { - console.error('Status update failed:', error); - }); - } catch (e) { - console.error('Log event handling failed:', e); - } - }); - - eventSource.addEventListener('run', (event) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - const content = data.result; - const timestamp = new Date().toLocaleTimeString(); - - const step = document.createElement('div'); - step.className = 'step-item run'; - step.innerHTML = ` -
- ${getEventIcon('run')} [${timestamp}] ${getEventLabel('run')}: -
${content}
-
- `; - - stepContainer.appendChild(step); - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); - - // Update task status - fetch(`/tasks/${taskId}`) - .then(response => response.json()) - .then(task => { - updateTaskStatus(task); - }) - .catch(error => { - console.error('Status update failed:', error); - }); - } catch (e) { - console.error('Run event handling failed:', e); - } - }); - - eventSource.addEventListener('message', (event) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - - // Create new step element - const step = document.createElement('div'); - step.className = `step-item ${data.type || 'step'}`; - - // Format content and timestamp - const content = data.result; - const timestamp = new Date().toLocaleTimeString(); - - step.innerHTML = ` -
- ${getEventIcon(data.type)} [${timestamp}] ${getEventLabel(data.type)}: -
${content}
-
- `; - - // Add step to container with animation - stepContainer.prepend(step); - setTimeout(() => { - step.classList.add('show'); - }, 10); - - // Auto-scroll to bottom - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); - } catch (e) { - console.error('Message handling failed:', e); - } - }); - - let isTaskComplete = false; - eventSource.addEventListener('complete', (event) => { - isTaskComplete = true; clearInterval(heartbeatTimer); clearInterval(pollInterval); container.innerHTML += ` @@ -408,7 +114,6 @@ function setupSSE(taskId) { `; eventSource.close(); currentEventSource = null; - lastResultContent = ''; // Clear result content }); eventSource.addEventListener('error', (event) => { @@ -427,17 +132,9 @@ function setupSSE(taskId) { console.error('Error handling failed:', e); } }); - } - - container.scrollTo({ - top: container.scrollHeight, - behavior: 'smooth' - }); eventSource.onerror = (err) => { - if (isTaskComplete) { - return; - } + if (eventSource.readyState === EventSource.CLOSED) return; console.error('SSE connection error:', err); clearInterval(heartbeatTimer); @@ -465,32 +162,105 @@ function setupSSE(taskId) { connect(); } -function getEventIcon(eventType) { - switch(eventType) { - case 'think': return '🤔'; - case 'tool': return '🛠️'; - case 'act': return '🚀'; - case 'result': return '🏁'; - case 'error': return '❌'; - case 'complete': return '✅'; - case 'log': return '📝'; - case 'run': return '⚙️'; - default: return 'ℹ️'; +function loadHistory() { + fetch('/tasks') + .then(response => { + if (!response.ok) { + return response.text().then(text => { + throw new Error(`请求失败: ${response.status} - ${text.substring(0, 100)}`); + }); + } + return response.json(); + }) + .then(tasks => { + const listContainer = document.getElementById('task-list'); + listContainer.innerHTML = tasks.map(task => ` +
+
${task.prompt}
+
+ ${new Date(task.created_at).toLocaleString()} - + + ${task.status || '未知状态'} + +
+
+ `).join(''); + }) + .catch(error => { + console.error('加载历史记录失败:', error); + const listContainer = document.getElementById('task-list'); + listContainer.innerHTML = `
加载失败: ${error.message}
`; + }); +} + + +function ensureStepContainer(container) { + let stepContainer = container.querySelector('.step-container'); + if (!stepContainer) { + container.innerHTML = '
'; + stepContainer = container.querySelector('.step-container'); } + return stepContainer; +} + +function formatStepContent(data, eventType) { + return { + formattedContent: data.result, + timestamp: new Date().toLocaleTimeString() + }; +} + +function createStepElement(type, content, timestamp) { + const step = document.createElement('div'); + step.className = `step-item ${type}`; + step.innerHTML = ` +
+ ${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}: +
${content}
+
+ `; + return step; +} + +function autoScroll(element) { + requestAnimationFrame(() => { + element.scrollTo({ + top: element.scrollHeight, + behavior: 'smooth' + }); + }); + setTimeout(() => { + element.scrollTop = element.scrollHeight; + }, 100); +} + + +function getEventIcon(eventType) { + const icons = { + 'think': '🤔', + 'tool': '🛠️', + 'act': '🚀', + 'result': '🏁', + 'error': '❌', + 'complete': '✅', + 'log': '📝', + 'run': '⚙️' + }; + return icons[eventType] || 'ℹ️'; } function getEventLabel(eventType) { - switch(eventType) { - case 'think': return 'Thinking'; - case 'tool': return 'Using Tool'; - case 'act': return 'Action'; - case 'result': return 'Result'; - case 'error': return 'Error'; - case 'complete': return 'Complete'; - case 'log': return 'Log'; - case 'run': return 'Running'; - default: return 'Info'; - } + const labels = { + 'think': 'Thinking', + 'tool': 'Using Tool', + 'act': 'Action', + 'result': 'Result', + 'error': 'Error', + 'complete': 'Complete', + 'log': 'Log', + 'run': 'Running' + }; + return labels[eventType] || 'Info'; } function updateTaskStatus(task) { @@ -506,164 +276,9 @@ function updateTaskStatus(task) { } } -function loadHistory() { - fetch('/tasks') - .then(response => { - if (!response.ok) { - throw new Error('Failed to load history'); - } - return response.json(); - }) - .then(tasks => { - const historyContainer = document.getElementById('history-container'); - if (!historyContainer) return; - - historyContainer.innerHTML = ''; - - if (tasks.length === 0) { - historyContainer.innerHTML = '
No recent tasks
'; - return; - } - - const historyList = document.createElement('div'); - historyList.className = 'history-list'; - - tasks.forEach(task => { - const taskItem = document.createElement('div'); - taskItem.className = `history-item ${task.status}`; - taskItem.innerHTML = ` -
${task.prompt}
-
- ${new Date(task.created_at).toLocaleString()} - ${getStatusIcon(task.status)} -
- `; - taskItem.addEventListener('click', () => { - loadTask(task.id); - }); - historyList.appendChild(taskItem); - }); - - historyContainer.appendChild(historyList); - }) - .catch(error => { - console.error('Failed to load history:', error); - const historyContainer = document.getElementById('history-container'); - if (historyContainer) { - historyContainer.innerHTML = `
Failed to load history: ${error.message}
`; - } - }); -} - -function getStatusIcon(status) { - switch(status) { - case 'completed': return '✅'; - case 'failed': return '❌'; - case 'running': return '⚙️'; - default: return '⏳'; - } -} - -function loadTask(taskId) { - if (currentEventSource) { - currentEventSource.close(); - currentEventSource = null; - } - - const container = document.getElementById('task-container'); - container.innerHTML = '
Loading task...
'; - document.getElementById('input-container').classList.add('bottom'); - - fetch(`/tasks/${taskId}`) - .then(response => { - if (!response.ok) { - throw new Error('Failed to load task'); - } - return response.json(); - }) - .then(task => { - if (task.status === 'running') { - setupSSE(taskId); - } else { - displayTask(task); - } - }) - .catch(error => { - console.error('Failed to load task:', error); - container.innerHTML = `
Failed to load task: ${error.message}
`; - }); -} - -function displayTask(task) { - const container = document.getElementById('task-container'); - container.innerHTML = ''; - container.classList.add('active'); - - const welcomeMessage = document.querySelector('.welcome-message'); - if (welcomeMessage) { - welcomeMessage.style.display = 'none'; - } - - const stepContainer = document.createElement('div'); - stepContainer.className = 'step-container'; - - if (task.steps && task.steps.length > 0) { - task.steps.forEach(step => { - const stepItem = document.createElement('div'); - stepItem.className = `step-item ${step.type || 'step'}`; - - const content = step.result; - const timestamp = new Date(step.timestamp || task.created_at).toLocaleTimeString(); - - stepItem.innerHTML = ` -
- ${getEventIcon(step.type)} [${timestamp}] ${getEventLabel(step.type)}: -
${content}
-
- `; - - stepContainer.appendChild(stepItem); - }); - } else { - stepContainer.innerHTML = '
No steps recorded for this task
'; - } - - container.appendChild(stepContainer); - - if (task.status === 'completed') { - let lastResultContent = ''; - if (task.steps && task.steps.length > 0) { - for (let i = task.steps.length - 1; i >= 0; i--) { - if (task.steps[i].type === 'result') { - lastResultContent = task.steps[i].result; - break; - } - } - } - - container.innerHTML += ` -
-
✅ Task completed
-
${lastResultContent}
-
- `; - } else if (task.status === 'failed') { - container.innerHTML += ` -
- ❌ Error: ${task.error || 'Unknown error'} -
- `; - } - - updateTaskStatus(task); -} - -// Initialize the app when the DOM is loaded document.addEventListener('DOMContentLoaded', () => { loadHistory(); - // Set up event listeners - document.getElementById('create-task-btn').addEventListener('click', createTask); document.getElementById('prompt-input').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -671,7 +286,6 @@ document.addEventListener('DOMContentLoaded', () => { } }); - // Show history button functionality const historyToggle = document.getElementById('history-toggle'); if (historyToggle) { historyToggle.addEventListener('click', () => { @@ -683,7 +297,6 @@ document.addEventListener('DOMContentLoaded', () => { }); } - // Clear button functionality const clearButton = document.getElementById('clear-btn'); if (clearButton) { clearButton.addEventListener('click', () => { diff --git a/static/style.css b/static/style.css index 776304e..5aaec28 100644 --- a/static/style.css +++ b/static/style.css @@ -28,6 +28,7 @@ body { .container { display: flex; min-height: 100vh; + min-width: 0; width: 90%; margin: 0 auto; padding: 20px; @@ -62,13 +63,13 @@ body { .task-container { @extend .card; width: 100%; + max-width: 100%; position: relative; min-height: 300px; display: flex; flex-direction: column; justify-content: center; align-items: center; - padding: 20px; overflow: auto; height: 100%; } @@ -177,11 +178,15 @@ button:hover { } .step-container { - display: block; + display: flex; + flex-direction: column; + gap: 10px; padding: 15px; width: 100%; - max-height: calc(100vh - 300px); + max-height: calc(100vh - 200px); overflow-y: auto; + max-width: 100%; + overflow-x: hidden; } .step-item { @@ -255,10 +260,9 @@ button:hover { border-left: 4px solid var(--text-light); } -/* 删除重复的样式定义 */ - .log-prefix { font-weight: bold; + white-space: nowrap; margin-bottom: 5px; color: #666; } @@ -267,15 +271,14 @@ button:hover { padding: 10px; border-radius: 4px; margin: 10px 0; - overflow-x: auto; - padding: 10px; - border-radius: 4px; - margin: 10px 0; - overflow-x: auto; + overflow-x: hidden; font-family: 'Courier New', monospace; font-size: 0.9em; line-height: 1.4; white-space: pre-wrap; + word-wrap: break-word; + word-break: break-all; + max-width: 100%; color: var(--text-color); background: var(--bg-color); @@ -336,3 +339,15 @@ button:hover { border-radius: 4px; margin: 10px 0; } + +pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; +} + +.complete pre { + max-width: 100%; + white-space: pre-wrap; + word-break: break-word; +} From 6e8de4e35e351a70881d06f4ae39e73ea18d336c Mon Sep 17 00:00:00 2001 From: liangxinbing <1580466765@qq.com> Date: Tue, 11 Mar 2025 10:56:57 +0800 Subject: [PATCH 11/13] update README --- README.md | 2 -- README_zh.md | 2 -- 2 files changed, 4 deletions(-) diff --git a/README.md b/README.md index 5075ae2..90fb40f 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,6 @@ English | [中文](README_zh.md) # 👋 OpenManus -[Official Website](https://openmanus.github.io/) - Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫! Our team diff --git a/README_zh.md b/README_zh.md index fef68c1..13bb768 100644 --- a/README_zh.md +++ b/README_zh.md @@ -7,8 +7,6 @@ # 👋 OpenManus -[官方网站](https://openmanus.github.io/) - Manus 非常棒,但 OpenManus 无需邀请码即可实现任何创意 🛫! 我们的团队成员 [@mannaandpoem](https://github.com/mannaandpoem) [@XiangJinyu](https://github.com/XiangJinyu) [@MoshiQAQ](https://github.com/MoshiQAQ) [@didiforgithub](https://github.com/didiforgithub) https://github.com/stellaHSR 来自 [@MetaGPT](https://github.com/geekan/MetaGPT) 组织,我们在 3 From 211d0694e31bafbced329fd080a5e39e478aa971 Mon Sep 17 00:00:00 2001 From: xiangjinyu <1376193973@qq.com> Date: Tue, 11 Mar 2025 11:46:57 +0800 Subject: [PATCH 12/13] update team members in README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 813f597..6039f0a 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,7 @@ English | [中文](README_zh.md) Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫! -Our team -members [@mannaandpoem](https://github.com/mannaandpoem) [@XiangJinyu](https://github.com/XiangJinyu) [@MoshiQAQ](https://github.com/MoshiQAQ) [@didiforgithub](https://github.com/didiforgithub) [@stellaHSR](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building! +Our team members [@Xinbin Liang](https://github.com/mannaandpoem) and [@Jinyu Xiang](https://github.com/XiangJinyu) (core authors), along with [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), and [@Sirui Hong](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building! It's a simple implementation, so we welcome any suggestions, contributions, and feedback! From 35de1f9e15f6b14f33ec00272aec59daf58db88d Mon Sep 17 00:00:00 2001 From: liangxinbing <1580466765@qq.com> Date: Tue, 11 Mar 2025 13:11:43 +0800 Subject: [PATCH 13/13] remove front-end code --- app.py | 255 ------------------------------- pytest.ini | 14 -- run.bat | 3 - static/main.js | 307 ------------------------------------- static/style.css | 353 ------------------------------------------- templates/index.html | 39 ----- 6 files changed, 971 deletions(-) delete mode 100644 app.py delete mode 100644 pytest.ini delete mode 100644 run.bat delete mode 100644 static/main.js delete mode 100644 static/style.css delete mode 100644 templates/index.html diff --git a/app.py b/app.py deleted file mode 100644 index 2e1959a..0000000 --- a/app.py +++ /dev/null @@ -1,255 +0,0 @@ -import asyncio -import threading -import uuid -import webbrowser -from datetime import datetime -from json import dumps - -from fastapi import Body, FastAPI, HTTPException, Request -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates -from pydantic import BaseModel - - -app = FastAPI() - -app.mount("/static", StaticFiles(directory="static"), name="static") -templates = Jinja2Templates(directory="templates") - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - - -class Task(BaseModel): - id: str - prompt: str - created_at: datetime - status: str - steps: list = [] - - def model_dump(self, *args, **kwargs): - data = super().model_dump(*args, **kwargs) - data["created_at"] = self.created_at.isoformat() - return data - - -class TaskManager: - def __init__(self): - self.tasks = {} - self.queues = {} - - def create_task(self, prompt: str) -> Task: - task_id = str(uuid.uuid4()) - task = Task( - id=task_id, prompt=prompt, created_at=datetime.now(), status="pending" - ) - self.tasks[task_id] = task - self.queues[task_id] = asyncio.Queue() - return task - - async def update_task_step( - self, task_id: str, step: int, result: str, step_type: str = "step" - ): - if task_id in self.tasks: - task = self.tasks[task_id] - task.steps.append({"step": step, "result": result, "type": step_type}) - await self.queues[task_id].put( - {"type": step_type, "step": step, "result": result} - ) - await self.queues[task_id].put( - {"type": "status", "status": task.status, "steps": task.steps} - ) - - async def complete_task(self, task_id: str): - if task_id in self.tasks: - task = self.tasks[task_id] - task.status = "completed" - await self.queues[task_id].put( - {"type": "status", "status": task.status, "steps": task.steps} - ) - await self.queues[task_id].put({"type": "complete"}) - - async def fail_task(self, task_id: str, error: str): - if task_id in self.tasks: - self.tasks[task_id].status = f"failed: {error}" - await self.queues[task_id].put({"type": "error", "message": error}) - - -task_manager = TaskManager() - - -@app.get("/", response_class=HTMLResponse) -async def index(request: Request): - return templates.TemplateResponse("index.html", {"request": request}) - - -@app.post("/tasks") -async def create_task(prompt: str = Body(..., embed=True)): - task = task_manager.create_task(prompt) - asyncio.create_task(run_task(task.id, prompt)) - return {"task_id": task.id} - - -from app.agent.manus import Manus - - -async def run_task(task_id: str, prompt: str): - try: - task_manager.tasks[task_id].status = "running" - - agent = Manus( - name="Manus", - description="A versatile agent that can solve various tasks using multiple tools", - max_steps=30, - ) - - async def on_think(thought): - await task_manager.update_task_step(task_id, 0, thought, "think") - - async def on_tool_execute(tool, input): - await task_manager.update_task_step( - task_id, 0, f"Executing tool: {tool}\nInput: {input}", "tool" - ) - - async def on_action(action): - await task_manager.update_task_step( - task_id, 0, f"Executing action: {action}", "act" - ) - - async def on_run(step, result): - await task_manager.update_task_step(task_id, step, result, "run") - - from app.logger import logger - - class SSELogHandler: - def __init__(self, task_id): - self.task_id = task_id - - async def __call__(self, message): - import re - - # 提取 - 后面的内容 - cleaned_message = re.sub(r"^.*? - ", "", message) - - event_type = "log" - if "✨ Manus's thoughts:" in cleaned_message: - event_type = "think" - elif "🛠️ Manus selected" in cleaned_message: - event_type = "tool" - elif "🎯 Tool" in cleaned_message: - event_type = "act" - elif "📝 Oops!" in cleaned_message: - event_type = "error" - elif "🏁 Special tool" in cleaned_message: - event_type = "complete" - - await task_manager.update_task_step( - self.task_id, 0, cleaned_message, event_type - ) - - sse_handler = SSELogHandler(task_id) - logger.add(sse_handler) - - result = await agent.run(prompt) - await task_manager.update_task_step(task_id, 1, result, "result") - await task_manager.complete_task(task_id) - except Exception as e: - await task_manager.fail_task(task_id, str(e)) - - -@app.get("/tasks/{task_id}/events") -async def task_events(task_id: str): - async def event_generator(): - if task_id not in task_manager.queues: - yield f"event: error\ndata: {dumps({'message': 'Task not found'})}\n\n" - return - - queue = task_manager.queues[task_id] - - task = task_manager.tasks.get(task_id) - if task: - yield f"event: status\ndata: {dumps({'type': 'status', 'status': task.status, 'steps': task.steps})}\n\n" - - while True: - try: - event = await queue.get() - formatted_event = dumps(event) - - yield ": heartbeat\n\n" - - if event["type"] == "complete": - yield f"event: complete\ndata: {formatted_event}\n\n" - break - elif event["type"] == "error": - yield f"event: error\ndata: {formatted_event}\n\n" - break - elif event["type"] == "step": - task = task_manager.tasks.get(task_id) - if task: - yield f"event: status\ndata: {dumps({'type': 'status', 'status': task.status, 'steps': task.steps})}\n\n" - yield f"event: {event['type']}\ndata: {formatted_event}\n\n" - elif event["type"] in ["think", "tool", "act", "run"]: - yield f"event: {event['type']}\ndata: {formatted_event}\n\n" - else: - yield f"event: {event['type']}\ndata: {formatted_event}\n\n" - - except asyncio.CancelledError: - print(f"Client disconnected for task {task_id}") - break - except Exception as e: - print(f"Error in event stream: {str(e)}") - yield f"event: error\ndata: {dumps({'message': str(e)})}\n\n" - break - - return StreamingResponse( - event_generator(), - media_type="text/event-stream", - headers={ - "Cache-Control": "no-cache", - "Connection": "keep-alive", - "X-Accel-Buffering": "no", - }, - ) - - -@app.get("/tasks") -async def get_tasks(): - sorted_tasks = sorted( - task_manager.tasks.values(), key=lambda task: task.created_at, reverse=True - ) - return JSONResponse( - content=[task.model_dump() for task in sorted_tasks], - headers={"Content-Type": "application/json"}, - ) - - -@app.get("/tasks/{task_id}") -async def get_task(task_id: str): - if task_id not in task_manager.tasks: - raise HTTPException(status_code=404, detail="Task not found") - return task_manager.tasks[task_id] - - -@app.exception_handler(Exception) -async def generic_exception_handler(request: Request, exc: Exception): - return JSONResponse( - status_code=500, content={"message": f"Server error: {str(exc)}"} - ) - - -def open_local_browser(): - webbrowser.open_new_tab("http://localhost:5172") - - -if __name__ == "__main__": - import uvicorn - - threading.Timer(3, open_local_browser).start() - uvicorn.run(app, host="localhost", port=5172) diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 4bbeada..0000000 --- a/pytest.ini +++ /dev/null @@ -1,14 +0,0 @@ -[pytest] -testpaths = tests -python_files = test_*.py -python_classes = Test* -python_functions = test_* - -# Log settings -log_cli = true -log_cli_level = INFO -log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) -log_cli_date_format = %Y-%m-%d %H:%M:%S - -# Make sure asyncio works properly -asyncio_mode = auto diff --git a/run.bat b/run.bat deleted file mode 100644 index 4c48f2f..0000000 --- a/run.bat +++ /dev/null @@ -1,3 +0,0 @@ -@echo off -venv\Scripts\python.exe app.py -pause diff --git a/static/main.js b/static/main.js deleted file mode 100644 index c219d20..0000000 --- a/static/main.js +++ /dev/null @@ -1,307 +0,0 @@ -let currentEventSource = null; - -function createTask() { - const promptInput = document.getElementById('prompt-input'); - const prompt = promptInput.value.trim(); - - if (!prompt) { - alert("Please enter a valid prompt"); - promptInput.focus(); - return; - } - - if (currentEventSource) { - currentEventSource.close(); - currentEventSource = null; - } - - const container = document.getElementById('task-container'); - container.innerHTML = '
Initializing task...
'; - document.getElementById('input-container').classList.add('bottom'); - - fetch('/tasks', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ prompt }) - }) - .then(response => { - if (!response.ok) { - return response.json().then(err => { throw new Error(err.detail || 'Request failed') }); - } - return response.json(); - }) - .then(data => { - if (!data.task_id) { - throw new Error('Invalid task ID'); - } - setupSSE(data.task_id); - loadHistory(); - }) - .catch(error => { - container.innerHTML = `
Error: ${error.message}
`; - console.error('Failed to create task:', error); - }); -} - -function setupSSE(taskId) { - let retryCount = 0; - const maxRetries = 3; - const retryDelay = 2000; - - const container = document.getElementById('task-container'); - - function connect() { - const eventSource = new EventSource(`/tasks/${taskId}/events`); - currentEventSource = eventSource; - - let heartbeatTimer = setInterval(() => { - container.innerHTML += '
·
'; - }, 5000); - - const pollInterval = setInterval(() => { - fetch(`/tasks/${taskId}`) - .then(response => response.json()) - .then(task => { - updateTaskStatus(task); - }) - .catch(error => { - console.error('Polling failed:', error); - }); - }, 10000); - - const handleEvent = (event, type) => { - clearInterval(heartbeatTimer); - try { - const data = JSON.parse(event.data); - container.querySelector('.loading')?.remove(); - container.classList.add('active'); - - const stepContainer = ensureStepContainer(container); - const { formattedContent, timestamp } = formatStepContent(data, type); - const step = createStepElement(type, formattedContent, timestamp); - - stepContainer.appendChild(step); - autoScroll(stepContainer); - - fetch(`/tasks/${taskId}`) - .then(response => response.json()) - .then(task => { - updateTaskStatus(task); - }) - .catch(error => { - console.error('Status update failed:', error); - }); - } catch (e) { - console.error(`Error handling ${type} event:`, e); - } - }; - - const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message']; - eventTypes.forEach(type => { - eventSource.addEventListener(type, (event) => handleEvent(event, type)); - }); - - eventSource.addEventListener('complete', (event) => { - clearInterval(heartbeatTimer); - clearInterval(pollInterval); - container.innerHTML += ` -
-
✅ Task completed
-
${lastResultContent}
-
- `; - eventSource.close(); - currentEventSource = null; - }); - - eventSource.addEventListener('error', (event) => { - clearInterval(heartbeatTimer); - clearInterval(pollInterval); - try { - const data = JSON.parse(event.data); - container.innerHTML += ` -
- ❌ Error: ${data.message} -
- `; - eventSource.close(); - currentEventSource = null; - } catch (e) { - console.error('Error handling failed:', e); - } - }); - - eventSource.onerror = (err) => { - if (eventSource.readyState === EventSource.CLOSED) return; - - console.error('SSE connection error:', err); - clearInterval(heartbeatTimer); - clearInterval(pollInterval); - eventSource.close(); - - if (retryCount < maxRetries) { - retryCount++; - container.innerHTML += ` -
- ⚠ Connection lost, retrying in ${retryDelay/1000} seconds (${retryCount}/${maxRetries})... -
- `; - setTimeout(connect, retryDelay); - } else { - container.innerHTML += ` -
- ⚠ Connection lost, please try refreshing the page -
- `; - } - }; - } - - connect(); -} - -function loadHistory() { - fetch('/tasks') - .then(response => { - if (!response.ok) { - return response.text().then(text => { - throw new Error(`请求失败: ${response.status} - ${text.substring(0, 100)}`); - }); - } - return response.json(); - }) - .then(tasks => { - const listContainer = document.getElementById('task-list'); - listContainer.innerHTML = tasks.map(task => ` -
-
${task.prompt}
-
- ${new Date(task.created_at).toLocaleString()} - - - ${task.status || '未知状态'} - -
-
- `).join(''); - }) - .catch(error => { - console.error('加载历史记录失败:', error); - const listContainer = document.getElementById('task-list'); - listContainer.innerHTML = `
加载失败: ${error.message}
`; - }); -} - - -function ensureStepContainer(container) { - let stepContainer = container.querySelector('.step-container'); - if (!stepContainer) { - container.innerHTML = '
'; - stepContainer = container.querySelector('.step-container'); - } - return stepContainer; -} - -function formatStepContent(data, eventType) { - return { - formattedContent: data.result, - timestamp: new Date().toLocaleTimeString() - }; -} - -function createStepElement(type, content, timestamp) { - const step = document.createElement('div'); - step.className = `step-item ${type}`; - step.innerHTML = ` -
- ${getEventIcon(type)} [${timestamp}] ${getEventLabel(type)}: -
${content}
-
- `; - return step; -} - -function autoScroll(element) { - requestAnimationFrame(() => { - element.scrollTo({ - top: element.scrollHeight, - behavior: 'smooth' - }); - }); - setTimeout(() => { - element.scrollTop = element.scrollHeight; - }, 100); -} - - -function getEventIcon(eventType) { - const icons = { - 'think': '🤔', - 'tool': '🛠️', - 'act': '🚀', - 'result': '🏁', - 'error': '❌', - 'complete': '✅', - 'log': '📝', - 'run': '⚙️' - }; - return icons[eventType] || 'ℹ️'; -} - -function getEventLabel(eventType) { - const labels = { - 'think': 'Thinking', - 'tool': 'Using Tool', - 'act': 'Action', - 'result': 'Result', - 'error': 'Error', - 'complete': 'Complete', - 'log': 'Log', - 'run': 'Running' - }; - return labels[eventType] || 'Info'; -} - -function updateTaskStatus(task) { - const statusBar = document.getElementById('status-bar'); - if (!statusBar) return; - - if (task.status === 'completed') { - statusBar.innerHTML = `✅ Task completed`; - } else if (task.status === 'failed') { - statusBar.innerHTML = `❌ Task failed: ${task.error || 'Unknown error'}`; - } else { - statusBar.innerHTML = `⚙️ Task running: ${task.status}`; - } -} - -document.addEventListener('DOMContentLoaded', () => { - loadHistory(); - - document.getElementById('prompt-input').addEventListener('keydown', (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - createTask(); - } - }); - - const historyToggle = document.getElementById('history-toggle'); - if (historyToggle) { - historyToggle.addEventListener('click', () => { - const historyPanel = document.getElementById('history-panel'); - if (historyPanel) { - historyPanel.classList.toggle('open'); - historyToggle.classList.toggle('active'); - } - }); - } - - const clearButton = document.getElementById('clear-btn'); - if (clearButton) { - clearButton.addEventListener('click', () => { - document.getElementById('prompt-input').value = ''; - document.getElementById('prompt-input').focus(); - }); - } -}); diff --git a/static/style.css b/static/style.css deleted file mode 100644 index 5aaec28..0000000 --- a/static/style.css +++ /dev/null @@ -1,353 +0,0 @@ -:root { - --primary-color: #007bff; - --primary-hover: #0056b3; - --success-color: #28a745; - --error-color: #dc3545; - --warning-color: #ff9800; - --info-color: #2196f3; - --text-color: #333; - --text-light: #666; - --bg-color: #f8f9fa; - --border-color: #ddd; -} - -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; - margin: 0; - padding: 0; - background-color: var(--bg-color); - color: var(--text-color); -} - -.container { - display: flex; - min-height: 100vh; - min-width: 0; - width: 90%; - margin: 0 auto; - padding: 20px; - gap: 20px; -} - -.card { - background-color: white; - border-radius: 8px; - padding: 20px; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); -} - -.history-panel { - width: 300px; -} - -.main-panel { - flex: 1; - display: flex; - flex-direction: column; - gap: 20px; -} - -.task-list { - margin-top: 10px; - max-height: calc(100vh - 160px); - overflow-y: auto; - overflow-x: hidden; -} - -.task-container { - @extend .card; - width: 100%; - max-width: 100%; - position: relative; - min-height: 300px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - overflow: auto; - height: 100%; -} - -.welcome-message { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - color: var(--text-light); - background: white; - z-index: 1; - padding: 20px; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); - width: 100%; - height: 100%; -} - -.welcome-message h1 { - font-size: 2rem; - margin-bottom: 10px; - color: var(--text-color); -} - -.input-container { - @extend .card; - display: flex; - gap: 10px; -} - -#prompt-input { - flex: 1; - padding: 12px; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 1rem; -} - -button { - padding: 12px 24px; - background-color: var(--primary-color); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; - transition: background-color 0.2s; -} - -button:hover { - background-color: var(--primary-hover); -} - -.task-item { - padding: 10px; - margin-bottom: 10px; - background-color: #f8f9fa; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.2s; -} - -.task-item:hover { - background-color: #e9ecef; -} - -.task-item.active { - background-color: var(--primary-color); - color: white; -} - -#input-container.bottom { - margin-top: auto; -} - -.task-card { - background: #fff; - padding: 15px; - margin-bottom: 10px; - border-radius: 8px; - cursor: pointer; - transition: all 0.2s; -} - -.task-card:hover { - transform: translateX(5px); - box-shadow: 0 2px 8px rgba(0,0,0,0.1); -} - -.status-pending { - color: var(--text-light); -} - -.status-running { - color: var(--primary-color); -} - -.status-completed { - color: var(--success-color); -} - -.status-failed { - color: var(--error-color); -} - -.step-container { - display: flex; - flex-direction: column; - gap: 10px; - padding: 15px; - width: 100%; - max-height: calc(100vh - 200px); - overflow-y: auto; - max-width: 100%; - overflow-x: hidden; -} - -.step-item { - padding: 15px; - background: white; - border-radius: 8px; - width: 100%; - box-shadow: 0 2px 4px rgba(0,0,0,0.05); - margin-bottom: 10px; - opacity: 1; - transform: none; -} - -.step-item .log-line:not(.result) { - opacity: 0.7; - color: #666; - font-size: 0.9em; -} - -.step-item .log-line.result { - opacity: 1; - color: #333; - font-size: 1em; - background: #e8f5e9; - border-left: 4px solid #4caf50; - padding: 10px; - border-radius: 4px; -} - -.step-item.show { - opacity: 1; - transform: none; -} - -.log-line { - padding: 10px; - border-radius: 4px; - margin-bottom: 10px; - display: flex; - flex-direction: column; - gap: 4px; -} - -.log-line.think, -.step-item pre.think { - background: var(--info-color-light); - border-left: 4px solid var(--info-color); -} - -.log-line.tool, -.step-item pre.tool { - background: var(--warning-color-light); - border-left: 4px solid var(--warning-color); -} - -.log-line.result, -.step-item pre.result { - background: var(--success-color-light); - border-left: 4px solid var(--success-color); -} - -.log-line.error, -.step-item pre.error { - background: var(--error-color-light); - border-left: 4px solid var(--error-color); -} - -.log-line.info, -.step-item pre.info { - background: var(--bg-color); - border-left: 4px solid var(--text-light); -} - -.log-prefix { - font-weight: bold; - white-space: nowrap; - margin-bottom: 5px; - color: #666; -} - -.step-item pre { - padding: 10px; - border-radius: 4px; - margin: 10px 0; - overflow-x: hidden; - font-family: 'Courier New', monospace; - font-size: 0.9em; - line-height: 1.4; - white-space: pre-wrap; - word-wrap: break-word; - word-break: break-all; - max-width: 100%; - color: var(--text-color); - background: var(--bg-color); - - &.log { - background: var(--bg-color); - border-left: 4px solid var(--text-light); - } - &.think { - background: var(--info-color-light); - border-left: 4px solid var(--info-color); - } - &.tool { - background: var(--warning-color-light); - border-left: 4px solid var(--warning-color); - } - &.result { - background: var(--success-color-light); - border-left: 4px solid var(--success-color); - } -} - -.step-item strong { - display: block; - margin-bottom: 8px; - color: #007bff; - font-size: 0.9em; -} - -.step-item div { - color: #444; - line-height: 1.6; -} - -.loading { - padding: 15px; - color: #666; - text-align: center; -} - -.ping { - color: #ccc; - text-align: center; - margin: 5px 0; -} - -.error { - color: #dc3545; - padding: 10px; - background: #ffe6e6; - border-radius: 4px; - margin: 10px 0; -} - -.complete { - color: #28a745; - padding: 10px; - background: #e6ffe6; - border-radius: 4px; - margin: 10px 0; -} - -pre { - margin: 0; - white-space: pre-wrap; - word-break: break-word; -} - -.complete pre { - max-width: 100%; - white-space: pre-wrap; - word-break: break-word; -} diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index d3245d9..0000000 --- a/templates/index.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - OpenManus Local Version - - - -
-
-

History Tasks

-
-
- -
-
-
-

Welcome to OpenManus Local Version

-

Please enter a task prompt to start a new task

-
-
-
- -
- - -
-
-
- - - -