├── .env.example
├── .github
└── workflows
│ └── docker-build-and-push.yml
├── .gitignore
├── README.md
├── README_zh.md
├── backend
├── Dockerfile
├── README.md
├── README_zh.md
├── app
│ ├── __init__.py
│ ├── application
│ │ ├── __init__.py
│ │ ├── errors
│ │ │ └── exceptions.py
│ │ └── services
│ │ │ ├── __init__.py
│ │ │ └── agent_service.py
│ ├── domain
│ │ ├── __init__.py
│ │ ├── events
│ │ │ ├── __init__.py
│ │ │ └── agent_events.py
│ │ ├── external
│ │ │ ├── __init__.py
│ │ │ ├── browser.py
│ │ │ ├── llm.py
│ │ │ ├── message_queue.py
│ │ │ ├── sandbox.py
│ │ │ ├── search.py
│ │ │ └── task.py
│ │ ├── models
│ │ │ ├── __init__.py
│ │ │ ├── agent.py
│ │ │ ├── memory.py
│ │ │ ├── plan.py
│ │ │ ├── session.py
│ │ │ └── tool_result.py
│ │ ├── repositories
│ │ │ ├── agent_repository.py
│ │ │ └── session_repository.py
│ │ ├── services
│ │ │ ├── __init__.py
│ │ │ ├── agent_domain_service.py
│ │ │ ├── agent_task_runner.py
│ │ │ ├── agents
│ │ │ │ ├── __init__.py
│ │ │ │ ├── base.py
│ │ │ │ ├── execution.py
│ │ │ │ └── planner.py
│ │ │ ├── flows
│ │ │ │ ├── __init__.py
│ │ │ │ ├── base.py
│ │ │ │ └── plan_act.py
│ │ │ ├── prompts
│ │ │ │ ├── __init__.py
│ │ │ │ ├── execution.py
│ │ │ │ └── planner.py
│ │ │ └── tools
│ │ │ │ ├── __init__.py
│ │ │ │ ├── base.py
│ │ │ │ ├── browser.py
│ │ │ │ ├── file.py
│ │ │ │ ├── message.py
│ │ │ │ ├── plan.py
│ │ │ │ ├── search.py
│ │ │ │ └── shell.py
│ │ └── utils
│ │ │ └── json_parser.py
│ ├── infrastructure
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── external
│ │ │ ├── __init__.py
│ │ │ ├── browser
│ │ │ │ └── playwright_browser.py
│ │ │ ├── llm
│ │ │ │ └── openai_llm.py
│ │ │ ├── message_queue
│ │ │ │ └── redis_stream_queue.py
│ │ │ ├── sandbox
│ │ │ │ └── docker_sandbox.py
│ │ │ ├── search
│ │ │ │ └── google_search.py
│ │ │ └── task
│ │ │ │ └── redis_task.py
│ │ ├── logging.py
│ │ ├── models
│ │ │ └── documents.py
│ │ ├── repositories
│ │ │ ├── mongo_agent_repository.py
│ │ │ └── mongo_session_repository.py
│ │ ├── storage
│ │ │ ├── mongodb.py
│ │ │ └── redis.py
│ │ └── utils
│ │ │ └── llm_json_parser.py
│ ├── interfaces
│ │ ├── api
│ │ │ └── routes.py
│ │ ├── errors
│ │ │ └── exception_handlers.py
│ │ └── schemas
│ │ │ ├── event.py
│ │ │ ├── request.py
│ │ │ └── response.py
│ └── main.py
├── dev.sh
├── requirements.txt
└── run.sh
├── build.sh
├── dev.sh
├── docker-compose-development.yml
├── docker-compose-example.yml
├── docker-compose.yml
├── frontend
├── .dockerignore
├── Dockerfile
├── README.md
├── README_zh.md
├── docker-entrypoint.sh
├── env.d.ts
├── index.html
├── nginx.conf
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── chatting.svg
│ └── favicon.ico
├── src
│ ├── App.vue
│ ├── api
│ │ ├── agent.ts
│ │ └── client.ts
│ ├── assets
│ │ ├── global.css
│ │ └── theme.css
│ ├── components
│ │ ├── BrowserToolView.vue
│ │ ├── ChatBox.vue
│ │ ├── ChatMessage.vue
│ │ ├── ContextMenu.vue
│ │ ├── CustomDialog.vue
│ │ ├── FileToolView.vue
│ │ ├── Panel.vue
│ │ ├── PlanPanel.vue
│ │ ├── SearchToolView.vue
│ │ ├── SessionItem.vue
│ │ ├── ShellToolView.vue
│ │ ├── SimpleBar.vue
│ │ ├── Toast.vue
│ │ ├── ToolPanel.vue
│ │ ├── ToolUse.vue
│ │ └── icons
│ │ │ ├── AttachmentIcon.vue
│ │ │ ├── BrowserIcon.vue
│ │ │ ├── EditIcon.vue
│ │ │ ├── ErrorIcon.vue
│ │ │ ├── InfoIcon.vue
│ │ │ ├── ManusIcon.vue
│ │ │ ├── ManusLogoTextIcon.vue
│ │ │ ├── ManusTextIcon.vue
│ │ │ ├── SearchIcon.vue
│ │ │ ├── SendIcon.vue
│ │ │ ├── ShellIcon.vue
│ │ │ ├── SpinnigIcon.vue
│ │ │ ├── StepSuccessIcon.vue
│ │ │ └── SuccessIcon.vue
│ ├── composables
│ │ ├── useContextMenu.ts
│ │ ├── useDialog.ts
│ │ ├── useI18n.ts
│ │ ├── usePanelState.ts
│ │ ├── useTime.ts
│ │ └── useTool.ts
│ ├── constants
│ │ └── tool.ts
│ ├── locales
│ │ ├── en.ts
│ │ ├── index.ts
│ │ └── zh.ts
│ ├── main.ts
│ ├── pages
│ │ ├── ChatPage.vue
│ │ └── HomePage.vue
│ ├── types
│ │ ├── event.ts
│ │ ├── message.ts
│ │ ├── panel.ts
│ │ └── response.ts
│ └── utils
│ │ ├── time.ts
│ │ └── toast.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
├── mockserver
├── Dockerfile
├── dev.sh
├── main.py
├── mock_datas
│ ├── browser_tools.yaml
│ ├── default.yaml
│ ├── file_tools.yaml
│ ├── message_tools.yaml
│ ├── search_tools.yaml
│ └── shell_tools.yaml
└── requirements.txt
├── run.sh
└── sandbox
├── Dockerfile
├── README.md
├── README_zh.md
├── app
├── __init__.py
├── api
│ ├── __init__.py
│ ├── router.py
│ └── v1
│ │ ├── __init__.py
│ │ ├── file.py
│ │ ├── shell.py
│ │ └── supervisor.py
├── core
│ ├── __init__.py
│ ├── config.py
│ ├── exceptions.py
│ └── middleware.py
├── main.py
├── models
│ ├── __init__.py
│ ├── file.py
│ ├── shell.py
│ └── supervisor.py
├── schemas
│ ├── __init__.py
│ ├── file.py
│ ├── response.py
│ └── shell.py
└── services
│ ├── __init__.py
│ ├── file.py
│ ├── shell.py
│ └── supervisor.py
├── requirements.txt
└── supervisord.conf
/.env.example:
--------------------------------------------------------------------------------
1 | # Model provider configuration
2 | API_KEY=
3 | API_BASE=http://mockserver:8090/v1
4 |
5 | # Model configuration
6 | MODEL_NAME=deepseek-chat
7 | TEMPERATURE=0.7
8 | MAX_TOKENS=2000
9 |
10 | # MongoDB configuration
11 | #MONGODB_URI=mongodb://mongodb:27017
12 | #MONGODB_DATABASE=manus
13 | #MONGODB_USERNAME=
14 | #MONGODB_PASSWORD=
15 |
16 | # Redis configuration
17 | #REDIS_HOST=redis
18 | #REDIS_PORT=6379
19 | #REDIS_DB=0
20 | #REDIS_PASSWORD=
21 |
22 | # Sandbox configuration
23 | #SANDBOX_ADDRESS=
24 | SANDBOX_IMAGE=simpleyyt/manus-sandbox
25 | SANDBOX_NAME_PREFIX=sandbox
26 | SANDBOX_TTL_MINUTES=30
27 | SANDBOX_NETWORK=manus-network
28 | #SANDBOX_CHROME_ARGS=
29 | #SANDBOX_HTTPS_PROXY=
30 | #SANDBOX_HTTP_PROXY=
31 | #SANDBOX_NO_PROXY=
32 |
33 | # Optional: Google search configuration
34 | #GOOGLE_SEARCH_API_KEY=
35 | #GOOGLE_SEARCH_ENGINE_ID=
36 |
37 | # Log configuration
38 | LOG_LEVEL=INFO
--------------------------------------------------------------------------------
/.github/workflows/docker-build-and-push.yml:
--------------------------------------------------------------------------------
1 | name: Build and Push Docker Images to Docker Hub
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - develop
8 | tags:
9 | - 'v*'
10 | pull_request:
11 | branches:
12 | - main
13 | - develop
14 |
15 | env:
16 | REGISTRY: docker.io
17 | IMAGE_REGISTRY: simpleyyt
18 |
19 | jobs:
20 | build-and-push:
21 | runs-on: ubuntu-latest
22 | strategy:
23 | matrix:
24 | component: [frontend, backend, sandbox]
25 |
26 | steps:
27 | - name: Checkout code
28 | uses: actions/checkout@v4
29 |
30 | - name: Set up Docker Buildx
31 | uses: docker/setup-buildx-action@v3
32 |
33 | - name: Log in to Docker Hub
34 | if: github.event_name != 'pull_request'
35 | uses: docker/login-action@v3
36 | with:
37 | registry: ${{ env.REGISTRY }}
38 | username: ${{ secrets.DOCKERHUB_USERNAME }}
39 | password: ${{ secrets.DOCKERHUB_TOKEN }}
40 |
41 | - name: Extract metadata
42 | id: meta
43 | uses: docker/metadata-action@v5
44 | with:
45 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_REGISTRY }}/manus-${{ matrix.component }}
46 | tags: |
47 | type=ref,event=branch
48 | type=ref,event=pr
49 | type=semver,pattern={{version}}
50 | type=semver,pattern={{major}}.{{minor}}
51 | type=semver,pattern={{major}}
52 | type=raw,value=latest,enable={{is_default_branch}}
53 |
54 | - name: Build and push Docker image
55 | uses: docker/build-push-action@v5
56 | with:
57 | context: ./${{ matrix.component }}
58 | file: ./${{ matrix.component }}/Dockerfile
59 | platforms: linux/amd64,linux/arm64
60 | push: ${{ github.event_name != 'pull_request' }}
61 | tags: ${{ steps.meta.outputs.tags }}
62 | labels: ${{ steps.meta.outputs.labels }}
63 | cache-from: type=gha
64 | cache-to: type=gha,mode=max
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.so
6 | .Python
7 | build/
8 | develop-eggs/
9 | dist/
10 | downloads/
11 | eggs/
12 | .eggs/
13 | lib/
14 | lib64/
15 | parts/
16 | sdist/
17 | var/
18 | wheels/
19 | *.egg-info/
20 | .installed.cfg
21 | *.egg
22 |
23 | # Virtual Environment
24 | venv/
25 | env/
26 | ENV/
27 | .env
28 | .venv
29 | env.bak/
30 | venv.bak/
31 |
32 | # IDE
33 | .idea/
34 | .cursor/
35 | .vscode/
36 | *.swp
37 | *.swo
38 | .DS_Store
39 |
40 | # Jupyter Notebook
41 | .ipynb_checkpoints
42 |
43 | # Testing
44 | .coverage
45 | htmlcov/
46 | .tox/
47 | .nox/
48 | .coverage.*
49 | .cache
50 | nosetests.xml
51 | coverage.xml
52 | *.cover
53 | *.py,cover
54 | .hypothesis/
55 | .pytest_cache/
56 |
57 | # Logs
58 | *.log
59 | logs/
60 |
61 | # Local development settings
62 | .env.local
63 | .env.development.local
64 | .env.test.local
65 | .env.production.local
66 |
67 | # Distribution / packaging
68 | .Python
69 | build/
70 | develop-eggs/
71 | dist/
72 | downloads/
73 | eggs/
74 | .eggs/
75 | lib/
76 | lib64/
77 | parts/
78 | sdist/
79 | var/
80 | wheels/
81 | *.egg-info/
82 | .installed.cfg
83 | *.egg
84 |
85 | # Unit test / coverage reports
86 | htmlcov/
87 | .tox/
88 | .coverage
89 | .coverage.*
90 | .cache
91 | nosetests.xml
92 | coverage.xml
93 | *.cover
94 | .hypothesis/
95 |
96 | # Node.js
97 | node_modules/
98 | npm-debug.log*
99 | yarn-debug.log*
100 | yarn-error.log*
101 | .pnpm-debug.log*
102 | .npm
103 | .yarn
104 | .yarnrc
105 | .pnp.*
106 | .next/
107 | out/
108 | .nuxt/
109 | dist/
110 | .cache/
111 | .parcel-cache/
112 | .vercel/
113 | .turbo/
114 | storybook-static/
115 | coverage/
116 | .nyc_output/
117 | .env*.local
118 | .vite/
119 | docker-compose-test.yml
120 | *.code-workspace
121 |
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.12-slim
2 |
3 | WORKDIR /app
4 |
5 | # Install dependencies
6 | COPY requirements.txt .
7 | RUN pip install --no-cache-dir -r requirements.txt
8 |
9 | # Copy project files
10 | COPY . .
11 |
12 | # Set script execution permissions
13 | RUN chmod +x run.sh
14 |
15 | # Set environment variables
16 | ENV PYTHONPATH=/app
17 |
18 | # Expose port
19 | EXPOSE 8000
20 |
21 | # Start command
22 | CMD ["./run.sh"]
--------------------------------------------------------------------------------
/backend/app/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Manus AI Agent Backend
3 | """
4 |
--------------------------------------------------------------------------------
/backend/app/application/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/application/__init__.py
--------------------------------------------------------------------------------
/backend/app/application/errors/exceptions.py:
--------------------------------------------------------------------------------
1 | class AppException(RuntimeError):
2 | def __init__(
3 | self,
4 | code: int,
5 | msg: str,
6 | status_code: int = 400,
7 | ):
8 | super().__init__(msg)
9 | self.code = code
10 | self.msg = msg
11 | self.status_code = status_code
12 |
13 |
14 | class NotFoundError(AppException):
15 | def __init__(self, msg: str = "Resource not found"):
16 | super().__init__(code=404, msg=msg, status_code=404)
17 |
18 |
19 | class BadRequestError(AppException):
20 | def __init__(self, msg: str = "Bad request parameters"):
21 | super().__init__(code=400, msg=msg, status_code=400)
22 |
23 |
24 | class ServerError(AppException):
25 | def __init__(self, msg: str = "Internal server error"):
26 | super().__init__(code=500, msg=msg, status_code=500)
27 |
28 |
29 | class UnauthorizedError(AppException):
30 | def __init__(self, msg: str = "Unauthorized"):
31 | super().__init__(code=401, msg=msg, status_code=401)
--------------------------------------------------------------------------------
/backend/app/application/services/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/application/services/__init__.py
--------------------------------------------------------------------------------
/backend/app/domain/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/domain/__init__.py
--------------------------------------------------------------------------------
/backend/app/domain/events/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/domain/events/__init__.py
--------------------------------------------------------------------------------
/backend/app/domain/events/agent_events.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, Field
2 | from typing import Dict, Any, Literal, Optional, Union
3 | from datetime import datetime
4 | import time
5 | import uuid
6 | from enum import Enum
7 | from app.domain.models.plan import Plan, Step
8 |
9 |
10 | class PlanStatus(str, Enum):
11 | """Plan status enum"""
12 | CREATED = "created"
13 | UPDATED = "updated"
14 | COMPLETED = "completed"
15 |
16 |
17 | class StepStatus(str, Enum):
18 | """Step status enum"""
19 | STARTED = "started"
20 | FAILED = "failed"
21 | COMPLETED = "completed"
22 |
23 |
24 | class ToolStatus(str, Enum):
25 | """Tool status enum"""
26 | CALLING = "calling"
27 | CALLED = "called"
28 |
29 |
30 | class BaseEvent(BaseModel):
31 | """Base class for agent events"""
32 | type: str
33 | id: str = Field(default_factory=lambda: str(uuid.uuid4()))
34 | timestamp: datetime = Field(default_factory=lambda: datetime.now())
35 |
36 | class ErrorEvent(BaseEvent):
37 | """Error event"""
38 | type: Literal["error"] = "error"
39 | error: str
40 |
41 | class PlanEvent(BaseEvent):
42 | """Plan related events"""
43 | type: Literal["plan"] = "plan"
44 | plan: Plan
45 | status: PlanStatus
46 | step: Optional[Step] = None
47 |
48 | class ToolEvent(BaseEvent):
49 | """Tool related events"""
50 | type: Literal["tool"] = "tool"
51 | tool_name: str
52 | function_name: str
53 | function_args: Dict[str, Any]
54 | status: ToolStatus
55 | function_result: Optional[Any] = None
56 |
57 | class TitleEvent(BaseEvent):
58 | """Title event"""
59 | type: Literal["title"] = "title"
60 | title: str
61 |
62 | class StepEvent(BaseEvent):
63 | """Step related events"""
64 | type: Literal["step"] = "step"
65 | step: Step
66 | status: StepStatus
67 |
68 | class MessageEvent(BaseEvent):
69 | """Message event"""
70 | type: Literal["message"] = "message"
71 | role: Literal["user", "assistant"] = "assistant"
72 | message: str
73 |
74 | class DoneEvent(BaseEvent):
75 | """Done event"""
76 | type: Literal["done"] = "done"
77 |
78 | AgentEvent = Union[
79 | BaseEvent,
80 | ErrorEvent,
81 | PlanEvent,
82 | ToolEvent,
83 | StepEvent,
84 | MessageEvent,
85 | DoneEvent,
86 | TitleEvent
87 | ]
88 |
89 |
90 | class AgentEventFactory:
91 | """Factory class for JSON conversion and AgentEvent manipulation"""
92 |
93 | @staticmethod
94 | def from_json(event_str: str) -> AgentEvent:
95 | """Create an AgentEvent from JSON string"""
96 | event = BaseEvent.model_validate_json(event_str)
97 |
98 | if (event.type == "plan"):
99 | return PlanEvent.model_validate_json(event_str)
100 | elif (event.type == "step"):
101 | return StepEvent.model_validate_json(event_str)
102 | elif (event.type == "tool"):
103 | return ToolEvent.model_validate_json(event_str)
104 | elif (event.type == "message"):
105 | return MessageEvent.model_validate_json(event_str)
106 | elif (event.type == "error"):
107 | return ErrorEvent.model_validate_json(event_str)
108 | elif (event.type == "done"):
109 | return DoneEvent.model_validate_json(event_str)
110 | elif (event.type == "title"):
111 | return TitleEvent.model_validate_json(event_str)
112 | else:
113 | return event
114 |
115 | @staticmethod
116 | def to_json(event: AgentEvent) -> str:
117 | """Convert an AgentEvent to JSON string"""
118 | return event.model_dump_json()
119 |
--------------------------------------------------------------------------------
/backend/app/domain/external/__init__.py:
--------------------------------------------------------------------------------
1 | from app.domain.external.llm import LLM
2 | from app.domain.external.sandbox import Sandbox
3 | from app.domain.external.browser import Browser
4 | from app.domain.external.search import SearchEngine
5 |
6 | __all__ = ['LLM', 'Sandbox', 'Browser', 'SearchEngine']
--------------------------------------------------------------------------------
/backend/app/domain/external/browser.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Protocol
2 | from app.domain.models.tool_result import ToolResult
3 |
4 | class Browser(Protocol):
5 | """Browser service gateway interface"""
6 |
7 | async def view_page(self) -> ToolResult:
8 | """View current page content"""
9 | ...
10 |
11 | async def navigate(self, url: str) -> ToolResult:
12 | """Navigate to specified URL"""
13 | ...
14 |
15 | async def restart(self, url: str) -> ToolResult:
16 | """Restart browser and navigate to specified URL"""
17 | ...
18 |
19 | async def click(
20 | self,
21 | index: Optional[int] = None,
22 | coordinate_x: Optional[float] = None,
23 | coordinate_y: Optional[float] = None
24 | ) -> ToolResult:
25 | """Click element"""
26 | ...
27 |
28 | async def input(
29 | self,
30 | text: str,
31 | press_enter: bool,
32 | index: Optional[int] = None,
33 | coordinate_x: Optional[float] = None,
34 | coordinate_y: Optional[float] = None
35 | ) -> ToolResult:
36 | """Input text"""
37 | ...
38 |
39 | async def move_mouse(
40 | self,
41 | coordinate_x: float,
42 | coordinate_y: float
43 | ) -> ToolResult:
44 | """Move mouse"""
45 | ...
46 |
47 | async def press_key(self, key: str) -> ToolResult:
48 | """Simulate key press"""
49 | ...
50 |
51 | async def select_option(
52 | self,
53 | index: int,
54 | option: int
55 | ) -> ToolResult:
56 | """Select dropdown option"""
57 | ...
58 |
59 | async def scroll_up(
60 | self,
61 | to_top: Optional[bool] = None
62 | ) -> ToolResult:
63 | """Scroll up"""
64 | ...
65 |
66 | async def scroll_down(
67 | self,
68 | to_bottom: Optional[bool] = None
69 | ) -> ToolResult:
70 | """Scroll down"""
71 | ...
72 |
73 | async def console_exec(self, javascript: str) -> ToolResult:
74 | """Execute JavaScript code"""
75 | ...
76 |
77 | async def console_view(self, max_lines: Optional[int] = None) -> ToolResult:
78 | """View console output"""
79 | ...
80 |
--------------------------------------------------------------------------------
/backend/app/domain/external/llm.py:
--------------------------------------------------------------------------------
1 | from typing import List, Dict, Any, Optional, Protocol
2 |
3 | class LLM(Protocol):
4 | """AI service gateway interface for interacting with AI services"""
5 |
6 | async def ask(
7 | self,
8 | messages: List[Dict[str, str]],
9 | tools: Optional[List[Dict[str, Any]]] = None,
10 | response_format: Optional[Dict[str, Any]] = None
11 | ) -> Dict[str, Any]:
12 | """Send chat request to AI service
13 |
14 | Args:
15 | messages: List of messages, including conversation history
16 | tools: Optional list of tools for function calling
17 | response_format: Optional response format configuration
18 |
19 | Returns:
20 | Response message from AI service
21 | """
22 | ...
23 |
24 | @property
25 | def model_name(self) -> str:
26 | """Get the model name"""
27 | ...
28 |
29 | @property
30 | def temperature(self) -> float:
31 | """Get the temperature"""
32 | ...
33 |
34 | @property
35 | def max_tokens(self) -> int:
36 | """Get the max tokens"""
37 | ...
--------------------------------------------------------------------------------
/backend/app/domain/external/message_queue.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Protocol, Tuple, Optional
2 |
3 | class MessageQueue(Protocol):
4 | """Message queue interface for agent communication"""
5 |
6 | async def put(self, message: Any) -> str:
7 | """Put a message into the queue
8 |
9 | Returns:
10 | str: Message ID
11 | """
12 | ...
13 |
14 | async def get(self, start_id: Optional[str] = None, block_ms: Optional[int] = None) -> Tuple[str, Any]:
15 | """Get a message from the queue
16 |
17 | Args:
18 | start_id: Message ID to start reading from, defaults to "0" meaning from the earliest message
19 | block_ms: Block time in milliseconds, defaults to None meaning no blocking
20 |
21 | Returns:
22 | Tuple[str, Any]: (Message ID, Message content), returns (None, None) if no message
23 | """
24 | ...
25 |
26 | async def pop(self) -> Tuple[str, Any]:
27 | """Get and remove the first message from the queue
28 |
29 | Returns:
30 | Tuple[str, Any]: (Message ID, Message content), returns (None, None) if queue is empty
31 | """
32 | ...
33 |
34 | async def clear(self) -> None:
35 | """Clear all messages from the queue"""
36 | ...
37 |
38 | async def is_empty(self) -> bool:
39 | """Check if the queue is empty"""
40 | ...
41 |
42 | async def size(self) -> int:
43 | """Get the current size of the queue"""
44 | ...
45 |
46 | async def delete_message(self, message_id: str) -> bool:
47 | """Delete a specific message from the queue
48 |
49 | Args:
50 | message_id: ID of the message to delete
51 |
52 | Returns:
53 | bool: True if message was deleted successfully, False otherwise
54 | """
55 | ...
56 |
--------------------------------------------------------------------------------
/backend/app/domain/external/search.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Protocol
2 | from app.domain.models.tool_result import ToolResult
3 |
4 | class SearchEngine(Protocol):
5 | """Search engine service gateway interface"""
6 |
7 | async def search(
8 | self,
9 | query: str,
10 | date_range: Optional[str] = None
11 | ) -> ToolResult:
12 | """Search webpages using search engine
13 |
14 | Args:
15 | query: Search query, Google search style, using 3-5 keywords
16 | date_range: (Optional) Time range filter for search results
17 |
18 | Returns:
19 | Search results
20 | """
21 | ...
--------------------------------------------------------------------------------
/backend/app/domain/external/task.py:
--------------------------------------------------------------------------------
1 | from typing import Protocol, Any, Awaitable, Optional, Callable
2 | from abc import ABC, abstractmethod
3 | from app.domain.external.message_queue import MessageQueue
4 |
5 |
6 | class TaskRunner(ABC):
7 | """Abstract base class defining the interface for task runners.
8 |
9 | This interface defines two essential lifecycle methods:
10 | - run: Main task execution logic
11 | - on_stop: Called when task execution needs to stop
12 | """
13 |
14 | @abstractmethod
15 | async def run(self, task: "Task") -> None:
16 | """Main task execution logic.
17 |
18 | This method contains the core functionality of the task.
19 | Implementations should handle setup, execution, and cleanup.
20 | """
21 | ...
22 |
23 | @abstractmethod
24 | async def destroy(self) -> None:
25 | """Destroy the task and release resources.
26 |
27 | Called when the task needs to be destroyed.
28 | This method is responsible for cleaning up and releasing all resources used by the task,
29 | including but not limited to:
30 | - Closing network connections
31 | - Freeing memory
32 | - Cleaning up temporary files
33 | - Stopping background processes etc.
34 | """
35 | ...
36 |
37 | @abstractmethod
38 | async def on_done(self, task: "Task") -> None:
39 | """Called when task execution is done.
40 |
41 | Use this method to handle graceful shutdown and cleanup.
42 | This method should ensure all resources are properly released.
43 | """
44 | ...
45 |
46 | class Task(Protocol):
47 | """Protocol defining the interface for task management operations."""
48 |
49 | async def run(self) -> None:
50 | """Run a task."""
51 | ...
52 |
53 | def cancel(self) -> bool:
54 | """Cancel a task.
55 |
56 | Returns:
57 | bool: True if the task is cancelled, False otherwise
58 | """
59 | ...
60 |
61 | @property
62 | def input_stream(self) -> MessageQueue:
63 | """Input stream."""
64 | ...
65 |
66 | @property
67 | def output_stream(self) -> MessageQueue:
68 | """Output stream."""
69 | ...
70 |
71 | @property
72 | def id(self) -> str:
73 | """Task ID."""
74 | ...
75 |
76 | @property
77 | def done(self) -> bool:
78 | """Check if the task is done.
79 |
80 | Returns:
81 | bool: True if the task is done, False otherwise
82 | """
83 | ...
84 |
85 | @classmethod
86 | def get(cls, task_id: str) -> Optional["Task"]:
87 | """Get a task by its ID.
88 |
89 | Returns:
90 | Optional[Task]: Task instance if found, None otherwise
91 | """
92 | ...
93 |
94 | @classmethod
95 | def create(cls, runner: TaskRunner) -> "Task":
96 | """Create a new task instance with the specified task runner.
97 |
98 | Args:
99 | runner (TaskRunner): The task runner that will execute this task
100 |
101 | Returns:
102 | Task: New task instance
103 | """
104 | ...
105 |
106 | @classmethod
107 | async def destroy(cls) -> None:
108 | """Destroy all task instances.
109 |
110 | Cleans up all running tasks and releases associated resources.
111 | """
112 | ...
113 |
--------------------------------------------------------------------------------
/backend/app/domain/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/domain/models/__init__.py
--------------------------------------------------------------------------------
/backend/app/domain/models/agent.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Dict
2 | from datetime import datetime, UTC
3 | from pydantic import BaseModel, Field, field_validator
4 | from app.domain.models.memory import Memory
5 | import uuid
6 |
7 | class Agent(BaseModel):
8 | """
9 | Agent aggregate root that manages the lifecycle and state of an AI agent
10 | Including its execution context, memory, and current plan
11 | """
12 | id: str = Field(default_factory=lambda: uuid.uuid4().hex[:16])
13 | memories: Dict[str, Memory] = Field(default_factory=dict)
14 | model_name: str = Field(default="")
15 | temperature: float = Field(default=0.7)
16 | max_tokens: int = Field(default=2000)
17 |
18 | # Context related fields
19 | created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) # Creation timestamp
20 | updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) # Last update timestamp
21 |
22 | @field_validator("temperature")
23 | def validate_temperature(cls, v: float) -> float:
24 | """Validate temperature is between 0 and 1"""
25 | if not 0 <= v <= 1:
26 | raise ValueError("Temperature must be between 0 and 1")
27 | return v
28 |
29 | @field_validator("max_tokens")
30 | def validate_max_tokens(cls, v: Optional[int]) -> Optional[int]:
31 | """Validate max_tokens is positive if provided"""
32 | if v is not None and v <= 0:
33 | raise ValueError("Max tokens must be positive")
34 | return v
35 |
36 | class Config:
37 | arbitrary_types_allowed = True
38 |
--------------------------------------------------------------------------------
/backend/app/domain/models/memory.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from typing import List, Dict, Any, Optional
3 |
4 | class Memory(BaseModel):
5 | """
6 | Memory class, defining the basic behavior of memory
7 | """
8 | messages: List[Dict[str, Any]] = []
9 |
10 | def get_message_role(self, message: Dict[str, Any]) -> str:
11 | """Get the role of the message"""
12 | return message.get("role")
13 |
14 | def add_message(self, message: Dict[str, Any]) -> None:
15 | """Add message to memory"""
16 | self.messages.append(message)
17 |
18 | def add_messages(self, messages: List[Dict[str, Any]]) -> None:
19 | """Add messages to memory"""
20 | self.messages.extend(messages)
21 |
22 | def get_messages(self) -> List[Dict[str, Any]]:
23 | """Get all message history"""
24 | return self.messages
25 |
26 | def get_latest_system_message(self) -> Dict[str, Any]:
27 | """Get the latest system message"""
28 | for message in reversed(self.messages):
29 | if self.get_message_role(message) == "system":
30 | return message
31 | return {}
32 |
33 | def get_non_system_messages(self) -> List[Dict[str, Any]]:
34 | """Get all non-system messages"""
35 | return [message for message in self.messages if self.get_message_role(message) != "system"]
36 |
37 | def get_messages_with_latest_system(self) -> List[Dict[str, Any]]:
38 | """Get all non-system messages plus the latest system message"""
39 | latest_system = self.get_latest_system_message()
40 | non_system_messages = self.get_non_system_messages()
41 | if latest_system:
42 | return [latest_system] + non_system_messages
43 | return non_system_messages
44 |
45 | def clear_messages(self) -> None:
46 | """Clear memory"""
47 | self.messages = []
48 |
49 | def get_filtered_messages(self) -> List[Dict[str, Any]]:
50 | """Get all non-system and non-tool response messages, plus the latest system message"""
51 | latest_system = self.get_latest_system_message()
52 | messages = [message for message in self.messages
53 | if self.get_message_role(message) != "system"]
54 | #and self.get_message_role(message) != "tool"]
55 | if latest_system:
56 | return [latest_system] + messages
57 | return messages
58 |
59 | def roll_back(self) -> None:
60 | """Roll back memory"""
61 | if len(self.messages) > 1 and \
62 | self.get_message_role(self.messages[-1]) == "tool" and \
63 | self.get_message_role(self.messages[-2]) != "tool":
64 | self.messages.pop()
65 | elif len(self.messages) > 0 and self.get_message_role(self.messages[-1]) == "user":
66 | self.messages.pop()
67 |
68 | @property
69 | def empty(self) -> bool:
70 | """Check if memory is empty"""
71 | return len(self.messages) == 0
72 |
--------------------------------------------------------------------------------
/backend/app/domain/models/plan.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from typing import List, Dict, Any, Optional
3 | from enum import Enum
4 |
5 | class ExecutionStatus(str, Enum):
6 | PENDING = "pending"
7 | RUNNING = "running"
8 | COMPLETED = "completed"
9 | FAILED = "failed"
10 |
11 | class Step(BaseModel):
12 | id: str
13 | description: str
14 | status: ExecutionStatus = ExecutionStatus.PENDING
15 | result: Optional[str] = None
16 | error: Optional[str] = None
17 |
18 | def is_done(self) -> bool:
19 | return self.status == ExecutionStatus.COMPLETED or self.status == ExecutionStatus.FAILED
20 |
21 | class Plan(BaseModel):
22 | id: str
23 | title: str
24 | goal: str
25 | steps: List[Step]
26 | message: Optional[str] = None
27 | status: ExecutionStatus = ExecutionStatus.PENDING
28 | result: Optional[Dict[str, Any]] = None
29 | error: Optional[str] = None
30 |
31 | def is_done(self) -> bool:
32 | return self.status == ExecutionStatus.COMPLETED or self.status == ExecutionStatus.FAILED
33 |
34 | def get_next_step(self) -> Optional[Step]:
35 | for step in self.steps:
36 | if not step.is_done():
37 | return step
38 | return None
39 |
--------------------------------------------------------------------------------
/backend/app/domain/models/session.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel, Field
2 | from datetime import datetime, UTC
3 | from typing import List, Optional
4 | from enum import Enum
5 | import uuid
6 | from app.domain.events.agent_events import BaseEvent
7 |
8 |
9 | class SessionStatus(str, Enum):
10 | """Session status enum"""
11 | ACTIVE = "active"
12 | COMPLETED = "completed"
13 |
14 |
15 | class Session(BaseModel):
16 | """Session model"""
17 | id: str = Field(default_factory=lambda: uuid.uuid4().hex[:16])
18 | sandbox_id: Optional[str] = Field(default=None) # Identifier for the sandbox environment
19 | agent_id: str
20 | task_id: Optional[str] = None
21 | title: Optional[str] = None
22 | unread_message_count: int = 0
23 | latest_message: Optional[str] = None
24 | latest_message_at: Optional[datetime] = Field(default_factory=lambda: datetime.now(UTC))
25 | created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
26 | updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
27 | events: List[BaseEvent] = []
28 | status: SessionStatus = SessionStatus.ACTIVE
--------------------------------------------------------------------------------
/backend/app/domain/models/tool_result.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from typing import Any, Optional
3 |
4 | class ToolResult(BaseModel):
5 | success: bool
6 | message: Optional[str] = None
7 | data: Optional[Any] = None
8 |
--------------------------------------------------------------------------------
/backend/app/domain/repositories/agent_repository.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List, Protocol
2 | from app.domain.models.agent import Agent
3 | from app.domain.models.plan import Plan
4 | from app.domain.models.memory import Memory
5 |
6 | class AgentRepository(Protocol):
7 | """Repository interface for Agent aggregate"""
8 |
9 | async def save(self, agent: Agent) -> None:
10 | """Save or update an agent"""
11 | ...
12 |
13 | async def find_by_id(self, agent_id: str) -> Optional[Agent]:
14 | """Find an agent by its ID"""
15 | ...
16 |
17 | async def add_memory(self, agent_id: str,
18 | name: str,
19 | memory: Memory) -> None:
20 | """Add or update a memory for an agent"""
21 | ...
22 |
23 | async def get_memory(self, agent_id: str, name: str) -> Memory:
24 | """Get memory by name from agent, create if not exists"""
25 | ...
26 |
27 | async def save_memory(self, agent_id: str, name: str, memory: Memory) -> None:
28 | """Update the messages of a memory"""
29 | ...
--------------------------------------------------------------------------------
/backend/app/domain/repositories/session_repository.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Protocol, List
2 | from datetime import datetime
3 | from app.domain.models.session import Session, SessionStatus
4 | from app.domain.events.agent_events import BaseEvent
5 |
6 | class SessionRepository(Protocol):
7 | """Repository interface for Session aggregate"""
8 |
9 | async def save(self, session: Session) -> None:
10 | """Save or update a session"""
11 | ...
12 |
13 | async def find_by_id(self, session_id: str) -> Optional[Session]:
14 | """Find a session by its ID"""
15 | ...
16 |
17 | async def update_title(self, session_id: str, title: str) -> None:
18 | """Update the title of a session"""
19 | ...
20 |
21 | async def update_latest_message(self, session_id: str, message: str, timestamp: datetime) -> None:
22 | """Update the latest message of a session"""
23 | ...
24 |
25 | async def add_event(self, session_id: str, event: BaseEvent) -> None:
26 | """Add an event to a session"""
27 | ...
28 |
29 | async def update_status(self, session_id: str, status: SessionStatus) -> None:
30 | """Update the status of a session"""
31 | ...
32 |
33 | async def update_unread_message_count(self, session_id: str, count: int) -> None:
34 | """Update the unread message count of a session"""
35 | ...
36 |
37 | async def increment_unread_message_count(self, session_id: str) -> None:
38 | """Increment the unread message count of a session"""
39 | ...
40 |
41 | async def decrement_unread_message_count(self, session_id: str) -> None:
42 | """Decrement the unread message count of a session"""
43 | ...
44 |
45 | async def delete(self, session_id: str) -> None:
46 | """Delete a session"""
47 | ...
48 |
49 | async def get_all(self) -> List[Session]:
50 | """Get all sessions"""
51 | ...
--------------------------------------------------------------------------------
/backend/app/domain/services/__init__.py:
--------------------------------------------------------------------------------
1 | from .agent_domain_service import AgentDomainService
2 |
3 | __all__ = [
4 | 'AgentDomainService'
5 | ]
6 |
--------------------------------------------------------------------------------
/backend/app/domain/services/agents/__init__.py:
--------------------------------------------------------------------------------
1 | from ...models.agent import Agent
2 |
3 | __all__ = [
4 | 'Agent'
5 | ]
6 |
--------------------------------------------------------------------------------
/backend/app/domain/services/agents/execution.py:
--------------------------------------------------------------------------------
1 | from typing import AsyncGenerator, Optional
2 | from app.domain.models.plan import Plan, Step, ExecutionStatus
3 | from app.domain.services.agents.base import BaseAgent
4 | from app.domain.external.llm import LLM
5 | from app.domain.external.sandbox import Sandbox
6 | from app.domain.external.browser import Browser
7 | from app.domain.external.search import SearchEngine
8 | from app.domain.repositories.agent_repository import AgentRepository
9 | from app.domain.services.prompts.execution import EXECUTION_SYSTEM_PROMPT, EXECUTION_PROMPT
10 | from app.domain.events.agent_events import (
11 | BaseEvent,
12 | StepEvent,
13 | StepStatus,
14 | ErrorEvent,
15 | MessageEvent,
16 | DoneEvent,
17 | )
18 | from app.domain.services.tools.shell import ShellTool
19 | from app.domain.services.tools.browser import BrowserTool
20 | from app.domain.services.tools.search import SearchTool
21 | from app.domain.services.tools.file import FileTool
22 | from app.domain.services.tools.message import MessageTool
23 | from app.domain.utils.json_parser import JsonParser
24 |
25 |
26 | class ExecutionAgent(BaseAgent):
27 | """
28 | Execution agent class, defining the basic behavior of execution
29 | """
30 |
31 | name: str = "execution"
32 | system_prompt: str = EXECUTION_SYSTEM_PROMPT
33 |
34 | def __init__(
35 | self,
36 | agent_id: str,
37 | agent_repository: AgentRepository,
38 | llm: LLM,
39 | sandbox: Sandbox,
40 | browser: Browser,
41 | json_parser: JsonParser,
42 | search_engine: Optional[SearchEngine] = None,
43 | ):
44 | super().__init__(
45 | agent_id=agent_id,
46 | agent_repository=agent_repository,
47 | llm=llm,
48 | json_parser=json_parser,
49 | tools=[
50 | ShellTool(sandbox),
51 | BrowserTool(browser),
52 | FileTool(sandbox),
53 | MessageTool()
54 | ]
55 | )
56 |
57 | # Only add search tool when search_engine is not None
58 | if search_engine:
59 | self.tools.append(SearchTool(search_engine))
60 |
61 | async def execute_step(self, plan: Plan, step: Step) -> AsyncGenerator[BaseEvent, None]:
62 | message = EXECUTION_PROMPT.format(goal=plan.goal, step=step.description)
63 | step.status = ExecutionStatus.RUNNING
64 | yield StepEvent(status=StepStatus.STARTED, step=step)
65 | async for event in self.execute(message):
66 | if isinstance(event, ErrorEvent):
67 | step.status = ExecutionStatus.FAILED
68 | step.error = event.error
69 | yield StepEvent(status=StepStatus.FAILED, step=step)
70 |
71 | if isinstance(event, MessageEvent):
72 | step.status = ExecutionStatus.COMPLETED
73 | step.result = event.message
74 | yield StepEvent(status=StepStatus.COMPLETED, step=step)
75 | yield event
76 | step.status = ExecutionStatus.COMPLETED
77 |
78 |
--------------------------------------------------------------------------------
/backend/app/domain/services/agents/planner.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Any, List, AsyncGenerator, Optional
2 | import json
3 | import logging
4 | from app.domain.models.plan import Plan, Step
5 | from app.domain.services.agents.base import BaseAgent
6 | from app.domain.models.memory import Memory
7 | from app.domain.external.llm import LLM
8 | from app.domain.services.prompts.planner import (
9 | PLANNER_SYSTEM_PROMPT,
10 | CREATE_PLAN_PROMPT,
11 | UPDATE_PLAN_PROMPT
12 | )
13 | from app.domain.events.agent_events import (
14 | BaseEvent,
15 | PlanEvent,
16 | PlanStatus,
17 | ErrorEvent,
18 | MessageEvent,
19 | DoneEvent,
20 | )
21 | from app.domain.external.sandbox import Sandbox
22 | from app.domain.services.tools.file import FileTool
23 | from app.domain.services.tools.shell import ShellTool
24 | from app.domain.repositories.agent_repository import AgentRepository
25 | from app.domain.utils.json_parser import JsonParser
26 |
27 | logger = logging.getLogger(__name__)
28 |
29 | class PlannerAgent(BaseAgent):
30 | """
31 | Planner agent class, defining the basic behavior of planning
32 | """
33 |
34 | name: str = "planner"
35 | system_prompt: str = PLANNER_SYSTEM_PROMPT
36 | format: Optional[str] = "json_object"
37 |
38 | def __init__(
39 | self,
40 | agent_id: str,
41 | agent_repository: AgentRepository,
42 | llm: LLM,
43 | json_parser: JsonParser,
44 | ):
45 | super().__init__(
46 | agent_id=agent_id,
47 | agent_repository=agent_repository,
48 | llm=llm,
49 | json_parser=json_parser,
50 | )
51 |
52 |
53 | async def create_plan(self, message: Optional[str] = None) -> AsyncGenerator[BaseEvent, None]:
54 | message = CREATE_PLAN_PROMPT.format(user_message=message) if message else None
55 | async for event in self.execute(message):
56 | if isinstance(event, MessageEvent):
57 | logger.info(event.message)
58 | parsed_response = await self.json_parser.parse(event.message)
59 | steps = [Step(id=step["id"], description=step["description"]) for step in parsed_response["steps"]]
60 | plan = Plan(id=f"plan_{len(steps)}", goal=parsed_response["goal"], title=parsed_response["title"], steps=steps, message=parsed_response["message"], todo=parsed_response.get("todo", ""))
61 | yield PlanEvent(status=PlanStatus.CREATED, plan=plan)
62 | else:
63 | yield event
64 |
65 | async def update_plan(self, plan: Plan) -> AsyncGenerator[BaseEvent, None]:
66 | message = UPDATE_PLAN_PROMPT.format(plan=plan.model_dump_json(include={"steps"}), goal=plan.goal)
67 | async for event in self.execute(message):
68 | if isinstance(event, MessageEvent):
69 | parsed_response = await self.json_parser.parse(event.message)
70 | new_steps = [Step(id=step["id"], description=step["description"]) for step in parsed_response["steps"]]
71 |
72 | # Find the index of the first pending step
73 | first_pending_index = None
74 | for i, step in enumerate(plan.steps):
75 | if not step.is_done():
76 | first_pending_index = i
77 | break
78 |
79 | # If there are pending steps, replace all pending steps
80 | if first_pending_index is not None:
81 | # Keep completed steps
82 | updated_steps = plan.steps[:first_pending_index]
83 | # Add new steps
84 | updated_steps.extend(new_steps)
85 | # Update steps in plan
86 | plan.steps = updated_steps
87 |
88 | yield PlanEvent(status=PlanStatus.UPDATED, plan=plan)
89 | else:
90 | yield event
--------------------------------------------------------------------------------
/backend/app/domain/services/flows/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/domain/services/flows/__init__.py
--------------------------------------------------------------------------------
/backend/app/domain/services/flows/base.py:
--------------------------------------------------------------------------------
1 | from app.domain.events.agent_events import BaseEvent
2 | from app.domain.models.agent import Agent
3 | from typing import AsyncGenerator
4 | from abc import ABC, abstractmethod
5 | from app.domain.repositories.agent_repository import AgentRepository
6 |
7 | class BaseFlow(ABC):
8 |
9 | @abstractmethod
10 | def run(self) -> AsyncGenerator[BaseEvent, None]:
11 | pass
12 |
13 | @abstractmethod
14 | def is_done(self) -> bool:
15 | pass
16 |
--------------------------------------------------------------------------------
/backend/app/domain/services/prompts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/domain/services/prompts/__init__.py
--------------------------------------------------------------------------------
/backend/app/domain/services/tools/__init__.py:
--------------------------------------------------------------------------------
1 | from app.domain.services.tools.base import BaseTool
2 | from app.domain.services.tools.browser import BrowserTool
3 | from app.domain.services.tools.shell import ShellTool
4 | from app.domain.services.tools.search import SearchTool
5 | from app.domain.services.tools.message import MessageTool
6 | from app.domain.services.tools.file import FileTool
7 |
8 | __all__ = [
9 | 'BaseTool',
10 | 'BrowserTool',
11 | 'ShellTool',
12 | 'SearchTool',
13 | 'MessageTool',
14 | 'FileTool',
15 | ]
16 |
--------------------------------------------------------------------------------
/backend/app/domain/services/tools/base.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Any, List, Callable
2 | import inspect
3 | from app.domain.models.tool_result import ToolResult
4 |
5 | def tool(
6 | name: str,
7 | description: str,
8 | parameters: Dict[str, Dict[str, Any]],
9 | required: List[str]
10 | ) -> Callable:
11 | """Tool registration decorator
12 |
13 | Args:
14 | name: Tool name
15 | description: Tool description
16 | parameters: Tool parameter definitions
17 | required: List of required parameters
18 |
19 | Returns:
20 | Decorator function
21 | """
22 | def decorator(func):
23 | # Create tool schema directly using provided parameters, without automatic extraction
24 | schema = {
25 | "type": "function",
26 | "function": {
27 | "name": name,
28 | "description": description,
29 | "parameters": {
30 | "type": "object",
31 | "properties": parameters,
32 | "required": required
33 | }
34 | }
35 | }
36 |
37 | # Store tool information
38 | func._function_name = name
39 | func._tool_description = description
40 | func._tool_schema = schema
41 |
42 | return func
43 |
44 | return decorator
45 |
46 | class BaseTool:
47 | """Base tool class, providing common tool calling methods"""
48 |
49 | name: str = ""
50 |
51 | def __init__(self):
52 | """Initialize base tool class"""
53 | self._tools_cache = None
54 |
55 | def get_tools(self) -> List[Dict[str, Any]]:
56 | """Get all registered tools
57 |
58 | Returns:
59 | List of tools
60 | """
61 | if self._tools_cache is not None:
62 | return self._tools_cache
63 |
64 | tools = []
65 | for _, method in inspect.getmembers(self, inspect.ismethod):
66 | if hasattr(method, '_tool_schema'):
67 | tools.append(method._tool_schema)
68 |
69 | self._tools_cache = tools
70 | return tools
71 |
72 | def has_function(self, function_name: str) -> bool:
73 | """Check if specified function exists
74 |
75 | Args:
76 | function_name: Function name
77 |
78 | Returns:
79 | Whether the tool exists
80 | """
81 | for _, method in inspect.getmembers(self, inspect.ismethod):
82 | if hasattr(method, '_function_name') and method._function_name == function_name:
83 | return True
84 | return False
85 |
86 | async def invoke_function(self, function_name: str, **kwargs) -> ToolResult:
87 | """Invoke specified tool
88 |
89 | Args:
90 | function_name: Function name
91 | **kwargs: Parameters
92 |
93 | Returns:
94 | Invocation result
95 |
96 | Raises:
97 | ValueError: Raised when tool doesn't exist
98 | """
99 | for _, method in inspect.getmembers(self, inspect.ismethod):
100 | if hasattr(method, '_function_name') and method._function_name == function_name:
101 | return await method(**kwargs)
102 |
103 | raise ValueError(f"Tool '{function_name}' not found")
--------------------------------------------------------------------------------
/backend/app/domain/services/tools/message.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional, Union
2 | from app.domain.services.tools.base import tool, BaseTool
3 | from app.domain.models.tool_result import ToolResult
4 |
5 | class MessageTool(BaseTool):
6 | """Message tool class, providing message sending functions for user interaction"""
7 |
8 | name: str = "message"
9 |
10 | def __init__(self):
11 | """Initialize message tool class"""
12 | super().__init__()
13 |
14 | @tool(
15 | name="message_notify_user",
16 | description="Send a message to user without requiring a response. Use for acknowledging receipt of messages, providing progress updates, reporting task completion, or explaining changes in approach.",
17 | parameters={
18 | "text": {
19 | "type": "string",
20 | "description": "Message text to display to user"
21 | }
22 | },
23 | required=["text"]
24 | )
25 | async def message_notify_user(
26 | self,
27 | text: str
28 | ) -> ToolResult:
29 | """Send notification message to user, no response needed
30 |
31 | Args:
32 | text: Message text to display to user
33 |
34 | Returns:
35 | Message sending result
36 | """
37 |
38 | # Return success result, actual UI display logic implemented by caller
39 | return ToolResult(
40 | success=True,
41 | data=text
42 | )
43 |
--------------------------------------------------------------------------------
/backend/app/domain/services/tools/plan.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/domain/services/tools/plan.py
--------------------------------------------------------------------------------
/backend/app/domain/services/tools/search.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from app.domain.external.search import SearchEngine
3 | from app.domain.services.tools.base import tool, BaseTool
4 | from app.domain.models.tool_result import ToolResult
5 |
6 | class SearchTool(BaseTool):
7 | """Search tool class, providing search engine interaction functions"""
8 |
9 | name: str = "search"
10 |
11 | def __init__(self, search_engine: SearchEngine):
12 | """Initialize search tool class
13 |
14 | Args:
15 | search_engine: Search engine service
16 | """
17 | super().__init__()
18 | self.search_engine = search_engine
19 |
20 | @tool(
21 | name="info_search_web",
22 | description="Search web pages using search engine. Use for obtaining latest information or finding references.",
23 | parameters={
24 | "query": {
25 | "type": "string",
26 | "description": "Search query in Google search style, using 3-5 keywords."
27 | },
28 | "date_range": {
29 | "type": "string",
30 | "enum": ["all", "past_hour", "past_day", "past_week", "past_month", "past_year"],
31 | "description": "(Optional) Time range filter for search results."
32 | }
33 | },
34 | required=["query"]
35 | )
36 | async def info_search_web(
37 | self,
38 | query: str,
39 | date_range: Optional[str] = None
40 | ) -> ToolResult:
41 | """Search webpages using search engine
42 |
43 | Args:
44 | query: Search query, Google search style, using 3-5 keywords
45 | date_range: (Optional) Time range filter for search results
46 |
47 | Returns:
48 | Search results
49 | """
50 | return await self.search_engine.search(query, date_range)
--------------------------------------------------------------------------------
/backend/app/domain/utils/json_parser.py:
--------------------------------------------------------------------------------
1 | from typing import Protocol, Optional, Any, Union, Dict, List
2 |
3 | class JsonParser(Protocol):
4 | """Json parser interface"""
5 |
6 | async def parse(self, text: str, default_value: Optional[Any] = None) -> Union[Dict, List, Any]:
7 | """
8 | Parse LLM output string to JSON using multiple strategies.
9 | Falls back to LLM parsing if local strategies fail.
10 |
11 | Args:
12 | text: The raw string output from LLM
13 | default_value: Default value to return if parsing fails
14 |
15 | Returns:
16 | Parsed JSON object (dict, list, or other JSON-serializable type)
17 |
18 | Raises:
19 | ValueError: If all parsing strategies fail and no default value provided
20 | """
21 |
--------------------------------------------------------------------------------
/backend/app/infrastructure/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Simpleyyt/ai-manus/16ff11f814a5c7fdfb771ad89a98e3dc27b94cc1/backend/app/infrastructure/__init__.py
--------------------------------------------------------------------------------
/backend/app/infrastructure/config.py:
--------------------------------------------------------------------------------
1 | from pydantic_settings import BaseSettings
2 | from functools import lru_cache
3 |
4 |
5 | class Settings(BaseSettings):
6 |
7 | # Model provider configuration
8 | api_key: str | None = None
9 | api_base: str = "https://api.deepseek.com/v1"
10 |
11 | # Model configuration
12 | model_name: str = "deepseek-chat"
13 | temperature: float = 0.7
14 | max_tokens: int = 2000
15 |
16 | # MongoDB configuration
17 | mongodb_uri: str = "mongodb://mongodb:27017"
18 | mongodb_database: str = "manus"
19 | mongodb_username: str | None = None
20 | mongodb_password: str | None = None
21 |
22 | # Redis configuration
23 | redis_host: str = "redis"
24 | redis_port: int = 6379
25 | redis_db: int = 0
26 | redis_password: str | None = None
27 |
28 | # Sandbox configuration
29 | sandbox_address: str | None = None
30 | sandbox_image: str | None = None
31 | sandbox_name_prefix: str | None = None
32 | sandbox_ttl_minutes: int | None = 30
33 | sandbox_network: str | None = None # Docker network bridge name
34 | sandbox_chrome_args: str | None = ""
35 | sandbox_https_proxy: str | None = None
36 | sandbox_http_proxy: str | None = None
37 | sandbox_no_proxy: str | None = None
38 |
39 | # Search engine configuration
40 | google_search_api_key: str | None = None
41 | google_search_engine_id: str | None = None
42 |
43 | # Logging configuration
44 | log_level: str = "INFO"
45 |
46 | class Config:
47 | env_file = ".env"
48 | env_file_encoding = "utf-8"
49 |
50 | def validate(self):
51 | if not self.api_key:
52 | raise ValueError("API key is required")
53 |
54 | @lru_cache()
55 | def get_settings() -> Settings:
56 | settings = Settings()
57 | settings.validate()
58 | return settings
59 |
--------------------------------------------------------------------------------
/backend/app/infrastructure/external/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/backend/app/infrastructure/external/llm/openai_llm.py:
--------------------------------------------------------------------------------
1 | from typing import List, Dict, Any, Optional
2 | from openai import AsyncOpenAI
3 | from app.domain.external.llm import LLM
4 | from app.infrastructure.config import get_settings
5 | import logging
6 |
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 | class OpenAILLM(LLM):
11 | def __init__(self):
12 | settings = get_settings()
13 | self.client = AsyncOpenAI(
14 | api_key=settings.api_key,
15 | base_url=settings.api_base
16 | )
17 |
18 | self._model_name = settings.model_name
19 | self._temperature = settings.temperature
20 | self._max_tokens = settings.max_tokens
21 | logger.info(f"Initialized OpenAI LLM with model: {self._model_name}")
22 |
23 | @property
24 | def model_name(self) -> str:
25 | return self._model_name
26 |
27 | @property
28 | def temperature(self) -> float:
29 | return self._temperature
30 |
31 | @property
32 | def max_tokens(self) -> int:
33 | return self._max_tokens
34 |
35 | async def ask(self, messages: List[Dict[str, str]],
36 | tools: Optional[List[Dict[str, Any]]] = None,
37 | response_format: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
38 | """Send chat request to OpenAI API"""
39 | response = None
40 | try:
41 | if tools:
42 | logger.debug(f"Sending request to OpenAI with tools, model: {self._model_name}")
43 | response = await self.client.chat.completions.create(
44 | model=self._model_name,
45 | temperature=self._temperature,
46 | max_tokens=self._max_tokens,
47 | messages=messages,
48 | tools=tools,
49 | response_format=response_format,
50 | )
51 | else:
52 | logger.debug(f"Sending request to OpenAI without tools, model: {self._model_name}")
53 | response = await self.client.chat.completions.create(
54 | model=self._model_name,
55 | temperature=self._temperature,
56 | max_tokens=self._max_tokens,
57 | messages=messages,
58 | response_format=response_format
59 | )
60 | return response.choices[0].message.model_dump()
61 | except Exception as e:
62 | logger.error(f"Error calling OpenAI API: {str(e)}")
63 | raise
--------------------------------------------------------------------------------
/backend/app/infrastructure/external/search/google_search.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | import logging
3 | import httpx
4 | from app.domain.models.tool_result import ToolResult
5 | from app.domain.external.search import SearchEngine
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 | class GoogleSearchEngine(SearchEngine):
10 | """Google API based search engine implementation"""
11 |
12 | def __init__(self, api_key: str, cx: str):
13 | """Initialize Google search engine
14 |
15 | Args:
16 | api_key: Google Custom Search API key
17 | cx: Google Search Engine ID
18 | """
19 | self.api_key = api_key
20 | self.cx = cx
21 | self.base_url = "https://www.googleapis.com/customsearch/v1"
22 |
23 | async def search(
24 | self,
25 | query: str,
26 | date_range: Optional[str] = None
27 | ) -> ToolResult:
28 | """Search web pages using Google API
29 |
30 | Args:
31 | query: Search query, Google search style, use 3-5 keywords
32 | date_range: (Optional) Time range filter for search results
33 |
34 | Returns:
35 | Search results
36 | """
37 | params = {
38 | "key": self.api_key,
39 | "cx": self.cx,
40 | "q": query
41 | }
42 |
43 | # Add time range filter
44 | if date_range and date_range != "all":
45 | # Convert date_range to time range parameters supported by Google API
46 | # For example: via dateRestrict parameter (d[number]: day, w[number]: week, m[number]: month, y[number]: year)
47 | date_mapping = {
48 | "past_hour": "d1",
49 | "past_day": "d1",
50 | "past_week": "w1",
51 | "past_month": "m1",
52 | "past_year": "y1"
53 | }
54 | if date_range in date_mapping:
55 | params["dateRestrict"] = date_mapping[date_range]
56 |
57 | try:
58 | async with httpx.AsyncClient() as client:
59 | response = await client.get(self.base_url, params=params)
60 | response.raise_for_status()
61 | data = response.json()
62 |
63 | # Process search results
64 | search_results = []
65 | if "items" in data:
66 | for item in data["items"]:
67 | search_results.append({
68 | "title": item.get("title", ""),
69 | "link": item.get("link", ""),
70 | "snippet": item.get("snippet", ""),
71 | })
72 |
73 | # Build return result
74 | results = {
75 | "query": query,
76 | "date_range": date_range,
77 | "search_info": data.get("searchInformation", {}),
78 | "results": search_results,
79 | "total_results": data.get("searchInformation", {}).get("totalResults", "0")
80 | }
81 |
82 | return ToolResult(success=True, data=results)
83 |
84 | except Exception as e:
85 | logger.error(f"Google Search API call failed: {e}")
86 | return ToolResult(
87 | success=False,
88 | message=f"Google Search API call failed: {e}",
89 | data={
90 | "query": query,
91 | "date_range": date_range,
92 | "results": []
93 | }
94 | )
95 |
--------------------------------------------------------------------------------
/backend/app/infrastructure/logging.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import sys
3 | from logging.handlers import RotatingFileHandler
4 | import os
5 | from .config import get_settings
6 |
7 | def setup_logging():
8 | """
9 | Configure the application logging system
10 |
11 | Sets up log levels, formatters, and handlers for both console and file output.
12 | Ensures proper log rotation to prevent log files from growing too large.
13 | """
14 | # Get configuration
15 | settings = get_settings()
16 |
17 | # Get root logger
18 | root_logger = logging.getLogger()
19 |
20 | # Set root log level
21 | log_level = getattr(logging, settings.log_level)
22 | root_logger.setLevel(log_level)
23 |
24 | # Create formatter
25 | formatter = logging.Formatter(
26 | '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
27 | datefmt='%Y-%m-%d %H:%M:%S'
28 | )
29 |
30 | # Create console handler
31 | console_handler = logging.StreamHandler(sys.stdout)
32 | console_handler.setFormatter(formatter)
33 | console_handler.setLevel(log_level)
34 |
35 | # Add handlers to root logger
36 | root_logger.addHandler(console_handler)
37 |
38 | # Disable verbose logging for pymongo
39 | logging.getLogger("pymongo").setLevel(logging.WARNING)
40 | logging.getLogger("websockets").setLevel(logging.WARNING)
41 |
42 | # Log initialization complete
43 | root_logger.info("Logging system initialized - Console and file logging active")
--------------------------------------------------------------------------------
/backend/app/infrastructure/models/documents.py:
--------------------------------------------------------------------------------
1 | from typing import Dict, Optional, List
2 | from datetime import datetime, timezone
3 | from beanie import Document
4 | from app.domain.models.memory import Memory
5 | from app.domain.events.agent_events import AgentEvent
6 | from app.domain.models.session import SessionStatus
7 |
8 | class AgentDocument(Document):
9 | """MongoDB document for Agent"""
10 | agent_id: str
11 | model_name: str
12 | temperature: float
13 | max_tokens: int
14 | memories: Dict[str, Memory] = {}
15 | created_at: datetime = datetime.now(timezone.utc)
16 | updated_at: datetime = datetime.now(timezone.utc)
17 |
18 | class Settings:
19 | name = "agents"
20 | indexes = [
21 | "agent_id",
22 | ]
23 |
24 |
25 | class SessionDocument(Document):
26 | """MongoDB model for Session"""
27 | session_id: str
28 | sandbox_id: Optional[str] = None
29 | agent_id: str
30 | task_id: Optional[str] = None
31 | title: Optional[str] = None
32 | unread_message_count: int = 0
33 | latest_message: Optional[str] = None
34 | latest_message_at: Optional[datetime] = None
35 | created_at: datetime = datetime.now(timezone.utc)
36 | updated_at: datetime = datetime.now(timezone.utc)
37 | events: List[AgentEvent]
38 | status: SessionStatus
39 |
40 | class Settings:
41 | name = "sessions"
42 | indexes = [
43 | "session_id",
44 | ]
--------------------------------------------------------------------------------
/backend/app/infrastructure/repositories/mongo_agent_repository.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List
2 | from datetime import datetime, UTC
3 | from app.domain.models.agent import Agent
4 | from app.domain.models.memory import Memory
5 | from app.domain.repositories.agent_repository import AgentRepository
6 | from app.infrastructure.models.documents import AgentDocument
7 | import logging
8 |
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 | class MongoAgentRepository(AgentRepository):
13 | """MongoDB implementation of AgentRepository"""
14 |
15 | async def save(self, agent: Agent) -> None:
16 | """Save or update an agent"""
17 | mongo_agent = await AgentDocument.find_one(
18 | AgentDocument.agent_id == agent.id
19 | )
20 |
21 | if not mongo_agent:
22 | mongo_agent = self._to_mongo_agent(agent)
23 | await mongo_agent.save()
24 | return
25 |
26 | # Use generic update method from base class
27 | mongo_agent.model_name=agent.model_name
28 | mongo_agent.temperature=agent.temperature
29 | mongo_agent.max_tokens=agent.max_tokens
30 | mongo_agent.memories=agent.memories
31 | mongo_agent.updated_at=datetime.now(UTC)
32 | await mongo_agent.save()
33 |
34 | async def find_by_id(self, agent_id: str) -> Optional[Agent]:
35 | """Find an agent by its ID"""
36 | mongo_agent = await AgentDocument.find_one(
37 | AgentDocument.agent_id == agent_id
38 | )
39 | return self._to_domain_agent(mongo_agent) if mongo_agent else None
40 |
41 | async def add_memory(self, agent_id: str,
42 | name: str,
43 | memory: Memory) -> None:
44 | """Add or update a memory for an agent"""
45 | result = await AgentDocument.find_one(
46 | AgentDocument.agent_id == agent_id
47 | ).update(
48 | {"$set": {f"memories.{name}": memory, "updated_at": datetime.now(UTC)}}
49 | )
50 | if not result:
51 | raise ValueError(f"Agent {agent_id} not found")
52 |
53 | async def get_memory(self, agent_id: str, name: str) -> Memory:
54 | """Get memory by name from agent, create if not exists"""
55 | mongo_agent = await AgentDocument.find_one(
56 | AgentDocument.agent_id == agent_id
57 | )
58 | if not mongo_agent:
59 | raise ValueError(f"Agent {agent_id} not found")
60 | return mongo_agent.memories.get(name, Memory(messages=[]))
61 |
62 | async def save_memory(self, agent_id: str, name: str, memory: Memory) -> None:
63 | """Update the messages of a memory"""
64 | result = await AgentDocument.find_one(
65 | AgentDocument.agent_id == agent_id
66 | ).update(
67 | {"$set": {f"memories.{name}": memory, "updated_at": datetime.now(UTC)}}
68 | )
69 | if not result:
70 | raise ValueError(f"Agent {agent_id} not found")
71 |
72 | def _to_domain_agent(self, mongo_agent: AgentDocument) -> Agent:
73 | """Convert MongoDB document to domain model"""
74 |
75 | return Agent(
76 | id=mongo_agent.agent_id,
77 | model_name=mongo_agent.model_name,
78 | temperature=mongo_agent.temperature,
79 | max_tokens=mongo_agent.max_tokens,
80 | created_at=mongo_agent.created_at,
81 | updated_at=mongo_agent.updated_at,
82 | memories=mongo_agent.memories
83 | )
84 |
85 |
86 | def _to_mongo_agent(self, agent: Agent) -> AgentDocument:
87 | """Create a new MongoDB agent from domain agent"""
88 | return AgentDocument(
89 | agent_id=agent.id,
90 | model_name=agent.model_name,
91 | temperature=agent.temperature,
92 | max_tokens=agent.max_tokens,
93 | created_at=agent.created_at,
94 | updated_at=agent.updated_at,
95 | memories=agent.memories
96 | )
--------------------------------------------------------------------------------
/backend/app/infrastructure/storage/mongodb.py:
--------------------------------------------------------------------------------
1 | from motor.motor_asyncio import AsyncIOMotorClient
2 | from pymongo.errors import ConnectionFailure
3 | from typing import Optional
4 | import logging
5 | from functools import lru_cache
6 | from app.infrastructure.config import get_settings
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 | class MongoDB:
11 | def __init__(self):
12 | self._client: Optional[AsyncIOMotorClient] = None
13 | self._settings = get_settings()
14 |
15 | async def initialize(self) -> None:
16 | """Initialize MongoDB connection and Beanie ODM."""
17 | if self._client is not None:
18 | return
19 |
20 | try:
21 | # Connect to MongoDB
22 | if self._settings.mongodb_username and self._settings.mongodb_password:
23 | # Use authenticated connection if username and password are configured
24 | self._client = AsyncIOMotorClient(
25 | self._settings.mongodb_uri,
26 | username=self._settings.mongodb_username,
27 | password=self._settings.mongodb_password,
28 | )
29 | else:
30 | # Use unauthenticated connection if no credentials are provided
31 | self._client = AsyncIOMotorClient(
32 | self._settings.mongodb_uri,
33 | )
34 | # Verify the connection
35 | await self._client.admin.command('ping')
36 | logger.info("Successfully connected to MongoDB")
37 | except ConnectionFailure as e:
38 | logger.error(f"Failed to connect to MongoDB: {str(e)}")
39 | raise
40 | except Exception as e:
41 | logger.error(f"Failed to initialize Beanie: {str(e)}")
42 | raise
43 |
44 | async def shutdown(self) -> None:
45 | """Shutdown MongoDB connection."""
46 | if self._client is not None:
47 | self._client.close()
48 | self._client = None
49 | logger.info("Disconnected from MongoDB")
50 | get_mongodb.cache_clear()
51 |
52 | @property
53 | def client(self) -> AsyncIOMotorClient:
54 | """Get the MongoDB client."""
55 | return self._client
56 |
57 |
58 | @lru_cache
59 | def get_mongodb() -> MongoDB:
60 | """Get the MongoDB instance."""
61 | return MongoDB()
62 |
63 |
--------------------------------------------------------------------------------
/backend/app/infrastructure/storage/redis.py:
--------------------------------------------------------------------------------
1 | from redis.asyncio import Redis
2 | from functools import lru_cache
3 | import logging
4 | from app.infrastructure.config import get_settings
5 |
6 | logger = logging.getLogger(__name__)
7 |
8 | class RedisClient:
9 | def __init__(self):
10 | self._client: Redis | None = None
11 | self._settings = get_settings()
12 |
13 | async def initialize(self) -> None:
14 | """Initialize Redis connection."""
15 | if self._client is not None:
16 | return
17 |
18 | try:
19 | # Connect to Redis
20 | self._client = Redis(
21 | host=self._settings.redis_host,
22 | port=self._settings.redis_port,
23 | db=self._settings.redis_db,
24 | password=self._settings.redis_password,
25 | decode_responses=True
26 | )
27 | # Verify the connection
28 | await self._client.ping()
29 | logger.info("Successfully connected to Redis")
30 | except Exception as e:
31 | logger.error(f"Failed to connect to Redis: {str(e)}")
32 | raise
33 |
34 | async def shutdown(self) -> None:
35 | """Shutdown Redis connection."""
36 | if self._client is not None:
37 | await self._client.close()
38 | self._client = None
39 | logger.info("Disconnected from Redis")
40 | get_redis.cache_clear()
41 |
42 | @property
43 | def client(self) -> Redis:
44 | """Get Redis client instance."""
45 | if self._client is None:
46 | raise RuntimeError("Redis client not initialized")
47 | return self._client
48 |
49 | @lru_cache
50 | def get_redis() -> RedisClient:
51 | """Get the Redis client instance."""
52 | return RedisClient()
--------------------------------------------------------------------------------
/backend/app/interfaces/errors/exception_handlers.py:
--------------------------------------------------------------------------------
1 | from fastapi import Request, FastAPI
2 | from fastapi.responses import JSONResponse
3 | import logging
4 | from starlette.exceptions import HTTPException as StarletteHTTPException
5 |
6 | from app.application.errors.exceptions import AppException
7 | from app.interfaces.schemas.response import APIResponse
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | def register_exception_handlers(app: FastAPI) -> None:
13 | """Register all exception handlers"""
14 |
15 | @app.exception_handler(AppException)
16 | async def api_exception_handler(request: Request, exc: AppException) -> JSONResponse:
17 | """Handle custom API exceptions"""
18 | logger.warning(f"APIException: {exc.msg}")
19 | return JSONResponse(
20 | status_code=exc.status_code,
21 | content=APIResponse(
22 | code=exc.code,
23 | msg=exc.msg,
24 | data=None
25 | ).model_dump(),
26 | )
27 |
28 | @app.exception_handler(StarletteHTTPException)
29 | async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
30 | """Handle HTTP exceptions"""
31 | logger.warning(f"HTTPException: {exc.detail}")
32 | return JSONResponse(
33 | status_code=exc.status_code,
34 | content=APIResponse(
35 | code=exc.status_code,
36 | msg=exc.detail,
37 | data=None
38 | ).model_dump(),
39 | )
40 |
41 | @app.exception_handler(Exception)
42 | async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse:
43 | """Handle all uncaught exceptions"""
44 | logger.exception(f"Unhandled exception: {str(exc)}")
45 | return JSONResponse(
46 | status_code=500,
47 | content=APIResponse(
48 | code=500,
49 | msg="Internal server error",
50 | data=None
51 | ).model_dump(),
52 | )
--------------------------------------------------------------------------------
/backend/app/interfaces/schemas/request.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from typing import Optional
3 |
4 | class ChatRequest(BaseModel):
5 | timestamp: Optional[int] = None
6 | message: Optional[str] = None
7 | event_id: Optional[str] = None
8 |
9 | class FileViewRequest(BaseModel):
10 | file: str
11 |
12 | class ShellViewRequest(BaseModel):
13 | session_id: str
--------------------------------------------------------------------------------
/backend/app/interfaces/schemas/response.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Generic, Optional, TypeVar, List
2 | from datetime import datetime
3 | from pydantic import BaseModel
4 | from app.interfaces.schemas.event import AgentSSEEvent
5 | from app.domain.models.session import SessionStatus
6 |
7 | T = TypeVar('T')
8 |
9 |
10 | class APIResponse(BaseModel, Generic[T]):
11 | code: int = 0
12 | msg: str = "success"
13 | data: Optional[T] = None
14 |
15 | @staticmethod
16 | def success(data: Optional[T] = None) -> "APIResponse[T]":
17 | return APIResponse(code=0, msg="success", data=data)
18 |
19 | @staticmethod
20 | def error(code: int, msg: str) -> "APIResponse[T]":
21 | return APIResponse(code=code, msg=msg, data=None)
22 |
23 |
24 | class CreateSessionResponse(BaseModel):
25 | session_id: str
26 |
27 | class GetSessionResponse(BaseModel):
28 | session_id: str
29 | title: Optional[str] = None
30 | events: List[AgentSSEEvent] = []
31 |
32 | class ListSessionItem(BaseModel):
33 | session_id: str
34 | title: Optional[str] = None
35 | latest_message: Optional[str] = None
36 | latest_message_at: Optional[int] = None
37 | status: SessionStatus
38 | unread_message_count: int
39 |
40 | class ListSessionResponse(BaseModel):
41 | sessions: List[ListSessionItem]
42 |
43 | class ConsoleRecord(BaseModel):
44 | ps1: str
45 | command: str
46 | output: str
47 |
48 | class ShellViewResponse(BaseModel):
49 | output: str
50 | session_id: str
51 | console: Optional[List[ConsoleRecord]] = None
52 |
53 | class FileViewResponse(BaseModel):
54 | content: str
55 | file: str
56 |
--------------------------------------------------------------------------------
/backend/dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
--------------------------------------------------------------------------------
/backend/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | uvicorn
3 | openai
4 | pydantic
5 | pydantic-settings
6 | python-dotenv
7 | sse-starlette
8 | httpx
9 | rich
10 | playwright>=1.42.0
11 | markdownify
12 | docker
13 | websockets
14 | motor>=3.3.2
15 | pymongo>=4.6.1
16 | beanie>=1.25.0
17 | async-lru>=2.0.0
18 | redis>=5.0.1
--------------------------------------------------------------------------------
/backend/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | exec uvicorn app.main:app --host 0.0.0.0 --port 8000
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | export BUILDX_NO_DEFAULT_ATTESTATIONS=1
4 | docker buildx bake "$@"
5 |
--------------------------------------------------------------------------------
/dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Determine which Docker Compose command to use
4 | if command -v docker &> /dev/null && docker compose version &> /dev/null; then
5 | COMPOSE="docker compose"
6 | elif command -v docker-compose &> /dev/null; then
7 | COMPOSE="docker-compose"
8 | else
9 | echo "Error: Neither docker compose nor docker-compose command found" >&2
10 | exit 1
11 | fi
12 |
13 |
14 | # Execute Docker Compose command
15 | $COMPOSE -f docker-compose-development.yml "$@"
16 |
--------------------------------------------------------------------------------
/docker-compose-development.yml:
--------------------------------------------------------------------------------
1 | services:
2 | frontend-dev:
3 | build:
4 | context: ./frontend
5 | dockerfile: Dockerfile
6 | target: build-stage # Use build stage image instead of production image
7 | command: ["npm", "run", "dev", "--", "--host", "0.0.0.0"] # Start in development mode and listen on all network interfaces
8 | volumes:
9 | - ./frontend:/app # Mount source code directory
10 | - /app/node_modules # Avoid overwriting container's node_modules
11 | ports:
12 | - "5173:5173" # Vite default development port
13 | environment:
14 | - NODE_ENV=development
15 | - VITE_API_URL=http://127.0.0.1:8000
16 | depends_on:
17 | - backend
18 | restart: unless-stopped
19 | networks:
20 | - manus-network
21 |
22 | backend:
23 | build:
24 | context: ./backend
25 | dockerfile: Dockerfile
26 | command: ["./dev.sh"] # Start in reload mode
27 | volumes:
28 | - ./backend:/app # Mount source code directory
29 | - /var/run/docker.sock:/var/run/docker.sock:ro
30 | - /app/__pycache__ # Avoid overwriting cache files
31 | - /app/.venv # Avoid overwriting virtual environment
32 | ports:
33 | - "8000:8000"
34 | depends_on:
35 | sandbox:
36 | condition: service_started
37 | required: false
38 | mongodb:
39 | condition: service_started
40 | required: true
41 | restart: unless-stopped
42 | networks:
43 | - manus-network
44 | env_file:
45 | - .env
46 | environment:
47 | - SANDBOX_ADDRESS=sandbox # Use single container as sandbox
48 |
49 | sandbox:
50 | build:
51 | context: ./sandbox
52 | dockerfile: Dockerfile
53 | hostname: sandbox
54 | volumes:
55 | - ./sandbox:/app # Mount source code directory
56 | - ./sandbox/supervisord.conf:/etc/supervisor/conf.d/app.conf
57 | - /app/__pycache__ # Avoid overwriting cache files
58 | - /app/.venv # Avoid overwriting virtual environment
59 | ports:
60 | #- "9222:9222"
61 | - "5902:5900"
62 | #- "5901:5901"
63 | - "8080:8080"
64 | environment:
65 | - UVI_ARGS="--reload"
66 | - LOG_LEVEL=${LOG_LEVEL:-DEBUG}
67 | restart: unless-stopped
68 | networks:
69 | - manus-network
70 |
71 | mockserver:
72 | build:
73 | context: ./mockserver
74 | dockerfile: Dockerfile
75 | volumes:
76 | - ./mockserver:/app # Mount source code directory
77 | - /app/__pycache__ # Avoid overwriting cache files
78 | - /app/.venv # Avoid overwriting virtual environment
79 | restart: unless-stopped
80 | environment:
81 | - MOCK_DATA_FILE=default.yaml
82 | - MOCK_DELAY=1
83 | networks:
84 | - manus-network
85 |
86 | mongodb:
87 | image: mongo:7.0
88 | volumes:
89 | - mongodb_data:/data/db
90 | restart: unless-stopped
91 | #ports:
92 | # - "27017:27017"
93 | networks:
94 | - manus-network
95 |
96 | redis:
97 | image: redis:7.0
98 | restart: unless-stopped
99 | networks:
100 | - manus-network
101 |
102 | volumes:
103 | mongodb_data:
104 | name: manus-mongodb-data
105 |
106 | networks:
107 | manus-network:
108 | name: manus-network
109 | driver: bridge
110 |
--------------------------------------------------------------------------------
/docker-compose-example.yml:
--------------------------------------------------------------------------------
1 | services:
2 | frontend:
3 | image: simpleyyt/manus-frontend
4 | ports:
5 | - "5173:80"
6 | depends_on:
7 | - backend
8 | restart: unless-stopped
9 | networks:
10 | - manus-network
11 | environment:
12 | - BACKEND_URL=http://backend:8000
13 |
14 | backend:
15 | image: simpleyyt/manus-backend
16 | depends_on:
17 | - sandbox
18 | restart: unless-stopped
19 | volumes:
20 | - /var/run/docker.sock:/var/run/docker.sock:ro
21 | networks:
22 | - manus-network
23 | environment:
24 | # OpenAI API base URL
25 | - API_BASE=https://api.openai.com/v1
26 | # OpenAI API key, replace with your own
27 | - API_KEY=sk-xxxx
28 | # LLM model name
29 | - MODEL_NAME=gpt-4o
30 | # LLM temperature parameter, controls randomness
31 | - TEMPERATURE=0.7
32 | # Maximum tokens for LLM response
33 | - MAX_TOKENS=2000
34 |
35 | # MongoDB connection URI (optional)
36 | #- MONGODB_URI=mongodb://mongodb:27017
37 | # MongoDB database name (optional)
38 | #- MONGODB_DATABASE=manus
39 | # MongoDB username (optional)
40 | #- MONGODB_USERNAME=
41 | # MongoDB password (optional)
42 | #- MONGODB_PASSWORD=
43 |
44 | # Redis server hostname (optional)
45 | #- REDIS_HOST=redis
46 | # Redis server port (optional)
47 | #- REDIS_PORT=6379
48 | # Redis database number (optional)
49 | #- REDIS_DB=0
50 | # Redis password (optional)
51 | #- REDIS_PASSWORD=
52 |
53 | # Sandbox server address (optional)
54 | #- SANDBOX_ADDRESS=
55 | # Docker image used for the sandbox
56 | - SANDBOX_IMAGE=simpleyyt/manus-sandbox
57 | # Prefix for sandbox container names
58 | - SANDBOX_NAME_PREFIX=sandbox
59 | # Time-to-live for sandbox containers in minutes
60 | - SANDBOX_TTL_MINUTES=30
61 | # Docker network for sandbox containers
62 | - SANDBOX_NETWORK=manus-network
63 | # Chrome browser arguments for sandbox (optional)
64 | #- SANDBOX_CHROME_ARGS=
65 | # HTTPS proxy for sandbox (optional)
66 | #- SANDBOX_HTTPS_PROXY=
67 | # HTTP proxy for sandbox (optional)
68 | #- SANDBOX_HTTP_PROXY=
69 | # No proxy hosts for sandbox (optional)
70 | #- SANDBOX_NO_PROXY=
71 |
72 | # Google Search API key for web search capability (optional)
73 | #- GOOGLE_SEARCH_API_KEY=
74 | # Google Custom Search Engine ID (optional)
75 | #- GOOGLE_SEARCH_ENGINE_ID=
76 |
77 | # Application log level
78 | - LOG_LEVEL=INFO
79 |
80 | sandbox:
81 | image: simpleyyt/manus-sandbox
82 | command: /bin/sh -c "exit 0" # prevent sandbox from starting, ensure image is pulled
83 | restart: "no"
84 | networks:
85 | - manus-network
86 |
87 | mongodb:
88 | image: mongo:7.0
89 | volumes:
90 | - mongodb_data:/data/db
91 | restart: unless-stopped
92 | #ports:
93 | # - "27017:27017"
94 | networks:
95 | - manus-network
96 |
97 | redis:
98 | image: redis:7.0
99 | restart: unless-stopped
100 | networks:
101 | - manus-network
102 |
103 | volumes:
104 | mongodb_data:
105 | name: manus-mongodb-data
106 |
107 | networks:
108 | manus-network:
109 | name: manus-network
110 | driver: bridge
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | frontend:
3 | image: ${IMAGE_REGISTRY:-simpleyyt}/manus-frontend:${IMAGE_TAG:-latest}
4 | build:
5 | context: ./frontend
6 | dockerfile: Dockerfile
7 | x-bake:
8 | platforms:
9 | - linux/amd64
10 | - linux/arm64
11 | ports:
12 | - "5173:80"
13 | depends_on:
14 | - backend
15 | restart: unless-stopped
16 | networks:
17 | - manus-network
18 | environment:
19 | - BACKEND_URL=http://backend:8000
20 |
21 | backend:
22 | image: ${IMAGE_REGISTRY:-simpleyyt}/manus-backend:${IMAGE_TAG:-latest}
23 | build:
24 | context: ./backend
25 | dockerfile: Dockerfile
26 | x-bake:
27 | platforms:
28 | - linux/amd64
29 | - linux/arm64
30 | depends_on:
31 | - sandbox
32 | restart: unless-stopped
33 | volumes:
34 | - /var/run/docker.sock:/var/run/docker.sock:ro
35 | networks:
36 | - manus-network
37 | env_file:
38 | - .env
39 |
40 | sandbox:
41 | image: ${IMAGE_REGISTRY:-simpleyyt}/manus-sandbox:${IMAGE_TAG:-latest}
42 | build:
43 | context: ./sandbox
44 | dockerfile: Dockerfile
45 | x-bake:
46 | platforms:
47 | - linux/amd64
48 | - linux/arm64
49 | command: /bin/sh -c "exit 0" # prevent sandbox from starting, ensure image is pulled
50 | restart: "no"
51 | networks:
52 | - manus-network
53 |
54 | mongodb:
55 | image: mongo:7.0
56 | volumes:
57 | - mongodb_data:/data/db
58 | restart: unless-stopped
59 | #ports:
60 | # - "27017:27017"
61 | networks:
62 | - manus-network
63 |
64 | redis:
65 | image: redis:7.0
66 | restart: unless-stopped
67 | networks:
68 | - manus-network
69 |
70 | volumes:
71 | mongodb_data:
72 | name: manus-mongodb-data
73 |
74 | networks:
75 | manus-network:
76 | name: manus-network
77 | driver: bridge
--------------------------------------------------------------------------------
/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | # 依赖目录
2 | node_modules
3 | npm-debug.log
4 | yarn-debug.log
5 | yarn-error.log
6 |
7 | # 编辑器和IDE文件
8 | .idea
9 | .vscode
10 | *.suo
11 | *.ntvs*
12 | *.njsproj
13 | *.sln
14 | *.sw?
15 |
16 | # 构建输出
17 | dist
18 | dist-ssr
19 | coverage
20 |
21 | # 本地环境文件
22 | .env.local
23 | .env.*.local
24 |
25 | # 日志
26 | logs
27 | *.log
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | pnpm-debug.log*
32 | lerna-debug.log*
33 |
34 | # 其他
35 | .DS_Store
36 | .git
37 | .gitignore
38 | README.md
39 | **/TEST*
40 | **/.env
--------------------------------------------------------------------------------
/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use Node.js as base image
2 | FROM node:18-alpine as build-stage
3 |
4 | # Set working directory
5 | WORKDIR /app
6 |
7 | # Copy package.json and package-lock.json files
8 | COPY package*.json ./
9 |
10 | # Install project dependencies
11 | RUN npm ci
12 |
13 | # Copy project files
14 | COPY . .
15 |
16 | # Build application - skip type checking
17 | RUN npm run build
18 |
19 | # Use nginx as production environment image
20 | FROM nginx:stable-alpine as production-stage
21 |
22 | # Copy built files to nginx
23 | COPY --from=build-stage /app/dist /usr/share/nginx/html
24 |
25 | # Copy nginx configuration
26 | COPY nginx.conf /etc/nginx/nginx.conf.template
27 |
28 | # Copy entrypoint script
29 | COPY docker-entrypoint.sh /docker-entrypoint.sh
30 | RUN chmod +x /docker-entrypoint.sh
31 |
32 | # Expose port 80
33 | EXPOSE 80
34 |
35 | # Start nginx with environment variable substitution
36 | CMD ["/docker-entrypoint.sh"]
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # AI Manus Frontend
2 |
3 | English | [中文](README_zh.md)
4 |
5 | This is an AI chatbot application built with Vue 3 + TypeScript + Vite. This project is ported from the React version, maintaining the same functionality and interface design.
6 |
7 | ## Features
8 |
9 | - Chat interface
10 | - Tool panels (Search, Files, Terminal, Browser)
11 |
12 | ## Installation
13 |
14 | Create a `.env.development` file with the following configuration:
15 |
16 | ```
17 | # Backend address
18 | VITE_API_URL=http://127.0.0.1:8000
19 | ```
20 |
21 | ```bash
22 | # Install dependencies
23 | npm install
24 |
25 | # Run in development mode
26 | npm run dev
27 |
28 | # Build production version
29 | npm run build
30 | ```
31 |
32 | ## Docker Deployment
33 |
34 | This project supports containerized deployment using Docker:
35 |
36 | ```bash
37 | # Build Docker image
38 | docker build -t ai-chatbot-vue .
39 |
40 | # Run container (map container port 80 to host port 8080)
41 | docker run -d -p 8080:80 ai-chatbot-vue
42 |
43 | # Access the application
44 | # Open browser and visit http://localhost:8080
45 | ```
46 |
47 | ## Project Structure
48 |
49 | ```
50 | src/
51 | ├── assets/ # Static resources and CSS files
52 | ├── components/ # Reusable components
53 | │ ├── ChatInput.vue # Chat input component
54 | │ ├── ChatMessage.vue # Chat message component
55 | │ ├── Sidebar.vue # Sidebar component
56 | │ ├── ToolPanel.vue # Tool panel component
57 | │ └── ui/ # UI components
58 | ├── pages/ # Page components
59 | │ ├── ChatPage.vue # Chat page
60 | │ └── HomePage.vue # Home page
61 | ├── App.vue # Root component
62 | ├── main.ts # Entry file
63 | └── index.css # Global styles
64 | ```
--------------------------------------------------------------------------------
/frontend/README_zh.md:
--------------------------------------------------------------------------------
1 | # AI Manus 前端
2 |
3 | [English](README.md) | 中文
4 |
5 | 这是一个使用 Vue 3 + TypeScript + Vite 构建的 AI 聊天机器人应用。该项目是从 React 版本移植过来的,保持了同样的功能和界面设计。
6 |
7 | ## 特性
8 |
9 | - 聊天界面
10 | - 工具面板(搜索、文件、终端、浏览器)
11 |
12 | ## 安装
13 |
14 | 创建`.env.development`文件,并创建以下配置:
15 |
16 | ```
17 | # 后端地址
18 | VITE_API_URL=http://127.0.0.1:8000
19 | ```
20 |
21 | ```bash
22 | # 安装依赖
23 | npm install
24 |
25 | # 开发模式运行
26 | npm run dev
27 |
28 | # 构建生产版本
29 | npm run build
30 | ```
31 |
32 | ## Docker 部署
33 |
34 | 本项目支持使用 Docker 进行容器化部署:
35 |
36 | ```bash
37 | # 构建 Docker 镜像
38 | docker build -t ai-chatbot-vue .
39 |
40 | # 运行容器(将容器的80端口映射到主机的8080端口)
41 | docker run -d -p 8080:80 ai-chatbot-vue
42 |
43 | # 访问应用
44 | # 打开浏览器访问 http://localhost:8080
45 | ```
46 |
47 | ## 项目结构
48 |
49 | ```
50 | src/
51 | ├── assets/ # 静态资源和CSS文件
52 | ├── components/ # 可复用组件
53 | │ ├── ChatInput.vue # 聊天输入组件
54 | │ ├── ChatMessage.vue # 聊天消息组件
55 | │ ├── Sidebar.vue # 侧边栏组件
56 | │ ├── ToolPanel.vue # 工具面板组件
57 | │ └── ui/ # UI组件
58 | ├── pages/ # 页面组件
59 | │ ├── ChatPage.vue # 聊天页面
60 | │ └── HomePage.vue # 首页
61 | ├── App.vue # 根组件
62 | ├── main.ts # 入口文件
63 | └── index.css # 全局样式
64 | ```
65 |
--------------------------------------------------------------------------------
/frontend/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Replace environment variables in nginx config
4 | envsubst '${BACKEND_URL}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
5 |
6 | # Start nginx
7 | nginx -g "daemon off;"
--------------------------------------------------------------------------------
/frontend/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
17 | 3 | {{ tool.args.text }} 4 |
5 |{{ toolInfo.functionArg }}
21 |