likhon saheikh commited on
Commit
ad8ba8a
·
1 Parent(s): 6fa1be7

Deploy full Gemini Agentic Platform

Browse files
Dockerfile CHANGED
@@ -1,7 +1,33 @@
1
  # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
  # you will also find guides on how best to write your Dockerfile
3
 
4
- FROM python:3.9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  RUN useradd -m -u 1000 user
7
  USER user
@@ -9,8 +35,20 @@ ENV PATH="/home/user/.local/bin:$PATH"
9
 
10
  WORKDIR /app
11
 
 
12
  COPY --chown=user ./requirements.txt requirements.txt
13
- RUN pip install --no-cache-dir --upgrade -r requirements.txt
 
 
 
 
 
 
 
14
 
15
  COPY --chown=user . /app
 
 
 
 
16
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
  # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
  # you will also find guides on how best to write your Dockerfile
3
 
4
+ FROM python:3.9-slim
5
+
6
+ # Install system dependencies
7
+ RUN apt-get update && apt-get install -y \
8
+ curl \
9
+ wget \
10
+ git \
11
+ build-essential \
12
+ libgl1-mesa-glx \
13
+ libglib2.0-0 \
14
+ libnss3 \
15
+ libatk-bridge2.0-0 \
16
+ libdrm2 \
17
+ libgtk-3-0 \
18
+ libxcomposite1 \
19
+ libxdamage1 \
20
+ libxrandr2 \
21
+ libgbm1 \
22
+ libasound2 \
23
+ && rm -rf /var/lib/apt/lists/*
24
+
25
+ # Install Chrome for Playwright
26
+ RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
27
+ && sh -c 'echo deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' \
28
+ && apt-get update \
29
+ && apt-get install -y google-chrome-stable \
30
+ && rm -rf /var/lib/apt/lists/*
31
 
32
  RUN useradd -m -u 1000 user
33
  USER user
 
35
 
36
  WORKDIR /app
37
 
38
+ # Copy requirements first for better caching
39
  COPY --chown=user ./requirements.txt requirements.txt
40
+
41
+ # Install Python dependencies
42
+ RUN pip install --no-cache-dir --upgrade pip \
43
+ && pip install --no-cache-dir --upgrade -r requirements.txt
44
+
45
+ # Install Playwright browsers
46
+ RUN pip install --no-cache-dir playwright \
47
+ && playwright install chromium --with-deps
48
 
49
  COPY --chown=user . /app
50
+
51
+ # Create directories for screenshots and logs
52
+ RUN mkdir -p screenshots logs
53
+
54
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py CHANGED
@@ -1,7 +1,34 @@
 
1
  from fastapi import FastAPI
 
 
2
 
3
- app = FastAPI()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  @app.get("/")
6
- def greet_json():
7
- return {"Hello": "World!"}
 
 
 
 
1
+ import uvicorn
2
  from fastapi import FastAPI
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from src.interface.api.routes import router
5
 
6
+ # Import tools to register them (disabled for Hugging Face Spaces deployment)
7
+ # import src.infrastructure.tools.browser
8
+ # import src.infrastructure.tools.shell
9
+ # import src.infrastructure.tools.file
10
+
11
+ app = FastAPI(
12
+ title="Gemini Agentic Platform",
13
+ description="Intelligent conversation agent system based on Gemini 3",
14
+ version="1.0.0"
15
+ )
16
+
17
+ # CORS middleware
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["*"],
21
+ allow_credentials=True,
22
+ allow_methods=["*"],
23
+ allow_headers=["*"],
24
+ )
25
+
26
+ # Include routers
27
+ app.include_router(router, prefix="/api/v1")
28
 
29
  @app.get("/")
30
+ def root():
31
+ return {"message": "Gemini Agentic Platform is running", "docs_url": "/docs"}
32
+
33
+ if __name__ == "__main__":
34
+ uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True)
config/config.yaml ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Config file for Gemini Agentic Platform
2
+
3
+ # API Configuration
4
+ api:
5
+ api_key: "${GEMINI_API_KEY:-your_api_key_here}"
6
+ base_url: "${GEMINI_BASE_URL:-https://api.gemini3.com/v1}"
7
+ timeout: "${GEMINI_TIMEOUT:-30}"
8
+ max_retries: "${GEMINI_MAX_RETRIES:-3}"
9
+ rate_limit: "${GEMINI_RATE_LIMIT:-10}"
10
+
11
+ # Browser Configuration
12
+ browser:
13
+ headless: "${BROWSER_HEADLESS:-false}"
14
+ window_size: "${BROWSER_WINDOW_SIZE:-1920,1080}"
15
+ proxy: "${BROWSER_PROXY:-}"
16
+ user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
17
+
18
+ # Logging Configuration
19
+ logging:
20
+ level: "${LOG_LEVEL:-INFO}"
21
+ format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
22
+
23
+ # Hugging Face Spaces Configuration
24
+ spaces:
25
+ port: "${PORT:-7860}"
26
+ host: "${HOST:-0.0.0.0}"
27
+ debug: "${DEBUG:-false}"
28
+
29
+ # Mobile Configuration
30
+ mobile:
31
+ platform_name: "${MOBILE_PLATFORM_NAME:-Android}"
32
+ device_name: "${MOBILE_DEVICE_NAME:-Android Emulator}"
33
+ app_package: "${MOBILE_APP_PACKAGE:-com.android.chrome}"
34
+
35
+ # Screenshot Configuration
36
+ screenshots:
37
+ save_path: "${SCREENSHOT_SAVE_PATH:-screenshots}"
38
+ format: "${SCREENSHOT_FORMAT:-png}"
39
+ auto_screenshot: "${AUTO_SCREENSHOT:-true}"
40
+ quality: "${SCREENSHOT_QUALITY:-90}"
requirements.txt CHANGED
@@ -1,2 +1,14 @@
1
  fastapi
2
- uvicorn[standard]
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  fastapi
2
+ uvicorn[standard]
3
+ pydantic
4
+ python-dotenv
5
+ google-genai
6
+ playwright
7
+ termcolor==3.1.0
8
+ browserbase==1.4.0
9
+ rich
10
+ pytest
11
+ requests
12
+ hf-transfer
13
+ transformers
14
+ sentencepiece
src/application/__pycache__/chat_service.cpython-314.pyc ADDED
Binary file (2.92 kB). View file
 
src/application/__pycache__/session_manager.cpython-314.pyc ADDED
Binary file (6.32 kB). View file
 
src/application/chat_service.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import AsyncGenerator, Dict, Any
2
+ from src.domain.models import Session, Message, MessageRole
3
+ from src.domain.interfaces import AgentRepository, SessionRepository
4
+ from src.application.session_manager import SessionManager
5
+
6
+ class ChatService:
7
+ def __init__(self, session_manager: SessionManager, agent: AgentRepository):
8
+ self.session_manager = session_manager
9
+ self.agent = agent
10
+
11
+ async def chat(self, session_id: str, message_content: str) -> AsyncGenerator[Dict[str, Any], None]:
12
+ session = await self.session_manager.get_session_details(session_id)
13
+ if not session:
14
+ yield {"event": "error", "data": "Session not found"}
15
+ return
16
+
17
+ # Create user message
18
+ user_message = Message(role=MessageRole.USER, content=message_content)
19
+ session.messages.append(user_message)
20
+ await self.session_manager.repository.update_session(session)
21
+
22
+ # Stream response from agent
23
+ full_response = ""
24
+ async for event in self.agent.chat(session, user_message):
25
+ if event["event"] == "message":
26
+ full_response += event["data"]
27
+ yield event
28
+
29
+ # Save assistant message
30
+ if full_response:
31
+ assistant_message = Message(role=MessageRole.ASSISTANT, content=full_response)
32
+ session.messages.append(assistant_message)
33
+ await self.session_manager.repository.update_session(session)
src/application/session_manager.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ import time
3
+ from typing import List, Dict, Optional
4
+ from src.domain.models import Session, SessionStatus, Message
5
+ from src.domain.interfaces import SessionRepository
6
+
7
+ class InMemorySessionRepository(SessionRepository):
8
+ def __init__(self):
9
+ self._sessions: Dict[str, Session] = {}
10
+
11
+ async def create_session(self) -> Session:
12
+ session_id = str(uuid.uuid4())
13
+ session = Session(
14
+ session_id=session_id,
15
+ title="New Chat",
16
+ status=SessionStatus.ACTIVE
17
+ )
18
+ self._sessions[session_id] = session
19
+ return session
20
+
21
+ async def get_session(self, session_id: str) -> Optional[Session]:
22
+ return self._sessions.get(session_id)
23
+
24
+ async def list_sessions(self) -> List[Session]:
25
+ return list(self._sessions.values())
26
+
27
+ async def delete_session(self, session_id: str) -> None:
28
+ if session_id in self._sessions:
29
+ del self._sessions[session_id]
30
+
31
+ async def update_session(self, session: Session) -> None:
32
+ self._sessions[session.session_id] = session
33
+
34
+ class SessionManager:
35
+ def __init__(self, repository: SessionRepository):
36
+ self.repository = repository
37
+
38
+ async def create_new_session(self) -> Session:
39
+ return await self.repository.create_session()
40
+
41
+ async def get_session_details(self, session_id: str) -> Optional[Session]:
42
+ return await self.repository.get_session(session_id)
43
+
44
+ async def list_all_sessions(self) -> List[Session]:
45
+ return await self.repository.list_sessions()
46
+
47
+ async def delete_session(self, session_id: str) -> None:
48
+ await self.repository.delete_session(session_id)
49
+
50
+ async def stop_session(self, session_id: str) -> None:
51
+ session = await self.repository.get_session(session_id)
52
+ if session:
53
+ session.status = SessionStatus.STOPPED
54
+ await self.repository.update_session(session)
55
+
56
+ # Singleton instance for simplicity in this demo
57
+ session_repository = InMemorySessionRepository()
58
+ session_manager = SessionManager(session_repository)
src/domain/__pycache__/interfaces.cpython-314.pyc ADDED
Binary file (4.82 kB). View file
 
src/domain/__pycache__/models.cpython-314.pyc ADDED
Binary file (6.92 kB). View file
 
src/domain/interfaces.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Dict, Any, AsyncGenerator
3
+ from src.domain.models import Session, Message, SessionStatus
4
+
5
+ class AgentRepository(ABC):
6
+ @abstractmethod
7
+ async def chat(self, session: Session, message: Message) -> AsyncGenerator[Dict[str, Any], None]:
8
+ """Send a message to the agent and get a stream of events"""
9
+ pass
10
+
11
+ class SandboxService(ABC):
12
+ @abstractmethod
13
+ async def execute_shell(self, session_id: str, command: str) -> str:
14
+ pass
15
+
16
+ @abstractmethod
17
+ async def read_file(self, session_id: str, path: str) -> str:
18
+ pass
19
+
20
+ @abstractmethod
21
+ async def start_session(self, session_id: str) -> None:
22
+ pass
23
+
24
+ @abstractmethod
25
+ async def stop_session(self, session_id: str) -> None:
26
+ pass
27
+
28
+ class SessionRepository(ABC):
29
+ @abstractmethod
30
+ async def create_session(self) -> Session:
31
+ pass
32
+
33
+ @abstractmethod
34
+ async def get_session(self, session_id: str) -> Session:
35
+ pass
36
+
37
+ @abstractmethod
38
+ async def list_sessions(self) -> List[Session]:
39
+ pass
40
+
41
+ @abstractmethod
42
+ async def delete_session(self, session_id: str) -> None:
43
+ pass
44
+
45
+ @abstractmethod
46
+ async def update_session(self, session: Session) -> None:
47
+ pass
src/domain/models.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional, Dict, Any
2
+ from pydantic import BaseModel, Field
3
+ from enum import Enum
4
+ import time
5
+
6
+ # --- Enums ---
7
+ class SessionStatus(str, Enum):
8
+ ACTIVE = "active"
9
+ STOPPED = "stopped"
10
+ ARCHIVED = "archived"
11
+
12
+ class MessageRole(str, Enum):
13
+ USER = "user"
14
+ ASSISTANT = "assistant"
15
+ SYSTEM = "system"
16
+ TOOL = "tool"
17
+
18
+ # --- Entities ---
19
+ class Message(BaseModel):
20
+ role: MessageRole
21
+ content: str
22
+ timestamp: int = Field(default_factory=lambda: int(time.time()))
23
+ tool_calls: Optional[List[Dict[str, Any]]] = None
24
+ tool_call_id: Optional[str] = None
25
+
26
+ class Session(BaseModel):
27
+ session_id: str
28
+ title: str
29
+ created_at: int = Field(default_factory=lambda: int(time.time()))
30
+ updated_at: int = Field(default_factory=lambda: int(time.time()))
31
+ status: SessionStatus = SessionStatus.ACTIVE
32
+ messages: List[Message] = []
33
+ metadata: Dict[str, Any] = {}
34
+
35
+ # --- API Request/Response Models ---
36
+
37
+ class StandardResponse(BaseModel):
38
+ code: int
39
+ msg: str
40
+ data: Optional[Any] = None
41
+
42
+ class SessionResponse(BaseModel):
43
+ session_id: str
44
+ title: str
45
+ latest_message: Optional[str] = None
46
+ latest_message_at: Optional[int] = None
47
+ status: SessionStatus
48
+ unread_message_count: int = 0
49
+
50
+ class SessionDetailResponse(BaseModel):
51
+ session_id: str
52
+ title: str
53
+ events: List[Dict[str, Any]] = [] # Simplified for now, can be specific Event models
54
+
55
+ class ChatRequest(BaseModel):
56
+ message: str
57
+ timestamp: Optional[int] = None
58
+ event_id: Optional[str] = None
59
+
60
+ class ShellRequest(BaseModel):
61
+ session_id: str
62
+
63
+ class FileRequest(BaseModel):
64
+ file: str
65
+
66
+ class ShellResponse(BaseModel):
67
+ output: str
68
+ session_id: str
69
+ console: List[Dict[str, str]]
70
+
71
+ class FileResponse(BaseModel):
72
+ content: str
73
+ file: str
src/infrastructure/llm/__pycache__/gemini_client.cpython-314.pyc ADDED
Binary file (5.9 kB). View file
 
src/infrastructure/llm/gemini_client.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import httpx
4
+ from typing import AsyncGenerator, Dict, Any
5
+ from src.domain.interfaces import AgentRepository
6
+ from src.domain.models import Session, Message, MessageRole
7
+ from src.infrastructure.tools.registry import tool_registry
8
+
9
+ class GeminiAgent(AgentRepository):
10
+ def __init__(self, api_key: str, model_name: str = "gemini-2.0-flash-exp"):
11
+ self.api_key = api_key
12
+ self.model_name = model_name
13
+ self.base_url = "https://generativelanguage.googleapis.com/v1beta/models"
14
+
15
+ async def chat(self, session: Session, message: Message) -> AsyncGenerator[Dict[str, Any], None]:
16
+ # Convert session history to Gemini format
17
+ contents = []
18
+ for msg in session.messages:
19
+ role = "user" if msg.role == MessageRole.USER else "model"
20
+ contents.append({"role": role, "parts": [{"text": msg.content}]})
21
+
22
+ # Add current message
23
+ contents.append({"role": "user", "parts": [{"text": message.content}]})
24
+
25
+ # Get tools
26
+ tools = tool_registry.to_gemini_tools()
27
+
28
+ # Prepare request
29
+ url = f"{self.base_url}/{self.model_name}:streamGenerateContent?alt=sse&key={self.api_key}"
30
+
31
+ payload = {
32
+ "contents": contents,
33
+ "tools": tools,
34
+ "generationConfig": {"temperature": 0.7}
35
+ }
36
+
37
+ async with httpx.AsyncClient() as client:
38
+ try:
39
+ async with client.stream("POST", url, json=payload, timeout=60.0) as response:
40
+ if response.status_code != 200:
41
+ error_text = await response.read()
42
+ error_msg = error_text.decode()
43
+ if response.status_code == 429:
44
+ yield {"event": "error", "data": "Gemini API Quota Exceeded. Please check your usage limits or try again later."}
45
+ else:
46
+ yield {"event": "error", "data": f"API Error {response.status_code}: {error_msg}"}
47
+ return
48
+
49
+ async for line in response.aiter_lines():
50
+ if line.startswith("data: "):
51
+ data_str = line[6:]
52
+ try:
53
+ chunk = json.loads(data_str)
54
+ # Parse candidates
55
+ if "candidates" in chunk and chunk["candidates"]:
56
+ candidate = chunk["candidates"][0]
57
+ if "content" in candidate and "parts" in candidate["content"]:
58
+ for part in candidate["content"]["parts"]:
59
+ if "text" in part:
60
+ yield {"event": "message", "data": part["text"]}
61
+ if "functionCall" in part:
62
+ fc = part["functionCall"]
63
+ yield {
64
+ "event": "tool",
65
+ "data": {"name": fc["name"], "args": fc["args"]}
66
+ }
67
+ # Execute tool
68
+ tool = tool_registry.get_tool(fc["name"])
69
+ if tool:
70
+ try:
71
+ result = await tool.func(**fc["args"])
72
+ yield {
73
+ "event": "tool_result",
74
+ "data": {"name": fc["name"], "result": result}
75
+ }
76
+ # Note: In a real implementation, we'd need to send this result back
77
+ # to the model in a new turn. For this demo, we just verify execution.
78
+ except Exception as e:
79
+ yield {"event": "error", "data": f"Tool execution failed: {e}"}
80
+ except json.JSONDecodeError:
81
+ pass
82
+ except Exception as e:
83
+ yield {"event": "error", "data": str(e)}
84
+
85
+ yield {"event": "done", "data": "stop"}
src/infrastructure/sandbox/__pycache__/docker_manager.cpython-314.pyc ADDED
Binary file (5.85 kB). View file
 
src/infrastructure/sandbox/docker_manager.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import docker
2
+ import tarfile
3
+ import io
4
+ from typing import Optional, Dict, List
5
+ from src.domain.interfaces import SandboxService
6
+
7
+ class DockerSandbox(SandboxService):
8
+ def __init__(self, image: str = "python:3.9-slim"):
9
+ self.client = docker.from_env()
10
+ self.image = image
11
+ self.containers: Dict[str, docker.models.containers.Container] = {}
12
+
13
+ async def start_session(self, session_id: str) -> None:
14
+ try:
15
+ container = self.client.containers.run(
16
+ self.image,
17
+ detach=True,
18
+ tty=True,
19
+ name=f"sandbox_{session_id}",
20
+ # Add resource limits or network isolation here if needed
21
+ )
22
+ self.containers[session_id] = container
23
+ except Exception as e:
24
+ print(f"Failed to start container for session {session_id}: {e}")
25
+ # In a real app, we might want to raise a custom exception
26
+
27
+ async def stop_session(self, session_id: str) -> None:
28
+ container = self.containers.get(session_id)
29
+ if container:
30
+ try:
31
+ container.stop()
32
+ container.remove()
33
+ del self.containers[session_id]
34
+ except Exception as e:
35
+ print(f"Failed to stop container for session {session_id}: {e}")
36
+
37
+ async def execute_shell(self, session_id: str, command: str) -> str:
38
+ container = self.containers.get(session_id)
39
+ if not container:
40
+ # Try to find existing container by name
41
+ try:
42
+ container = self.client.containers.get(f"sandbox_{session_id}")
43
+ self.containers[session_id] = container
44
+ except docker.errors.NotFound:
45
+ return "Error: Sandbox not running"
46
+
47
+ try:
48
+ exit_code, output = container.exec_run(command)
49
+ return output.decode("utf-8")
50
+ except Exception as e:
51
+ return f"Error executing command: {e}"
52
+
53
+ async def read_file(self, session_id: str, path: str) -> str:
54
+ container = self.containers.get(session_id)
55
+ if not container:
56
+ try:
57
+ container = self.client.containers.get(f"sandbox_{session_id}")
58
+ self.containers[session_id] = container
59
+ except docker.errors.NotFound:
60
+ return "Error: Sandbox not running"
61
+
62
+ try:
63
+ # get_archive returns a tuple (generator, stat)
64
+ bits, stat = container.get_archive(path)
65
+ file_obj = io.BytesIO()
66
+ for chunk in bits:
67
+ file_obj.write(chunk)
68
+ file_obj.seek(0)
69
+
70
+ with tarfile.open(fileobj=file_obj) as tar:
71
+ # Assuming single file request
72
+ member = tar.next()
73
+ f = tar.extractfile(member)
74
+ return f.read().decode("utf-8")
75
+ except Exception as e:
76
+ return f"Error reading file: {e}"
77
+
78
+ # Singleton for simplicity
79
+ # Note: In a real deployment, we'd handle docker connection errors gracefully
80
+ try:
81
+ docker_sandbox = DockerSandbox()
82
+ except Exception:
83
+ print("Warning: Docker not available. Sandbox features will fail.")
84
+ docker_sandbox = None
src/infrastructure/tools/__pycache__/browser.cpython-314.pyc ADDED
Binary file (1.82 kB). View file
 
src/infrastructure/tools/__pycache__/file.cpython-314.pyc ADDED
Binary file (1.16 kB). View file
 
src/infrastructure/tools/__pycache__/registry.cpython-314.pyc ADDED
Binary file (3.33 kB). View file
 
src/infrastructure/tools/__pycache__/shell.cpython-314.pyc ADDED
Binary file (1.2 kB). View file
 
src/infrastructure/tools/browser.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from playwright.async_api import async_playwright
2
+ from src.infrastructure.tools.registry import tool_registry
3
+
4
+ async def browser_navigate(url: str) -> str:
5
+ """Navigate to a URL and return the page content"""
6
+ async with async_playwright() as p:
7
+ browser = await p.chromium.launch(headless=True)
8
+ page = await browser.new_page()
9
+ await page.goto(url)
10
+ content = await page.content()
11
+ await browser.close()
12
+ return content[:2000] # Return first 2000 chars for now
13
+
14
+ tool_registry.register(
15
+ name="browser_navigate",
16
+ description="Navigate to a website and get its content",
17
+ parameters={
18
+ "type": "OBJECT",
19
+ "properties": {
20
+ "url": {"type": "STRING", "description": "The URL to navigate to"}
21
+ },
22
+ "required": ["url"]
23
+ },
24
+ func=browser_navigate
25
+ )
src/infrastructure/tools/file.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.infrastructure.tools.registry import tool_registry
2
+ from src.infrastructure.sandbox.docker_manager import docker_sandbox
3
+
4
+ async def file_read(session_id: str, path: str) -> str:
5
+ """Read a file from the sandbox"""
6
+ if not docker_sandbox:
7
+ return "Error: Sandbox unavailable"
8
+ return await docker_sandbox.read_file(session_id, path)
9
+
10
+ tool_registry.register(
11
+ name="file_read",
12
+ description="Read a file from the sandbox",
13
+ parameters={
14
+ "type": "OBJECT",
15
+ "properties": {
16
+ "session_id": {"type": "STRING", "description": "The session ID"},
17
+ "path": {"type": "STRING", "description": "The file path"}
18
+ },
19
+ "required": ["session_id", "path"]
20
+ },
21
+ func=file_read
22
+ )
src/infrastructure/tools/registry.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any, Callable, List
2
+ from pydantic import BaseModel
3
+
4
+ class Tool(BaseModel):
5
+ name: str
6
+ description: str
7
+ parameters: Dict[str, Any]
8
+ func: Callable
9
+
10
+ class ToolRegistry:
11
+ def __init__(self):
12
+ self._tools: Dict[str, Tool] = {}
13
+
14
+ def register(self, name: str, description: str, parameters: Dict[str, Any], func: Callable):
15
+ self._tools[name] = Tool(
16
+ name=name,
17
+ description=description,
18
+ parameters=parameters,
19
+ func=func
20
+ )
21
+
22
+ def get_tool(self, name: str) -> Tool:
23
+ return self._tools.get(name)
24
+
25
+ def list_tools(self) -> List[Tool]:
26
+ return list(self._tools.values())
27
+
28
+ def to_gemini_tools(self) -> List[Dict[str, Any]]:
29
+ # Convert to Gemini tool format
30
+ # This is a simplified conversion, actual schema mapping needed
31
+ tools = []
32
+ for tool in self._tools.values():
33
+ tools.append({
34
+ "function_declarations": [{
35
+ "name": tool.name,
36
+ "description": tool.description,
37
+ "parameters": tool.parameters
38
+ }]
39
+ })
40
+ return tools
41
+
42
+ tool_registry = ToolRegistry()
src/infrastructure/tools/shell.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.infrastructure.tools.registry import tool_registry
2
+ from src.infrastructure.sandbox.docker_manager import docker_sandbox
3
+
4
+ async def shell_execute(session_id: str, command: str) -> str:
5
+ """Execute a shell command in the sandbox"""
6
+ if not docker_sandbox:
7
+ return "Error: Sandbox unavailable"
8
+ return await docker_sandbox.execute_shell(session_id, command)
9
+
10
+ tool_registry.register(
11
+ name="shell_execute",
12
+ description="Execute a shell command in the sandbox",
13
+ parameters={
14
+ "type": "OBJECT",
15
+ "properties": {
16
+ "session_id": {"type": "STRING", "description": "The session ID"},
17
+ "command": {"type": "STRING", "description": "The command to execute"}
18
+ },
19
+ "required": ["session_id", "command"]
20
+ },
21
+ func=shell_execute
22
+ )
src/interface/api/__pycache__/routes.cpython-314.pyc ADDED
Binary file (8.31 kB). View file
 
src/interface/api/routes.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, BackgroundTasks
2
+ from typing import List, Optional
3
+ from pydantic import BaseModel
4
+
5
+ router = APIRouter()
6
+
7
+ from src.domain.models import (
8
+ StandardResponse, SessionResponse, SessionDetailResponse,
9
+ ChatRequest, ShellRequest, FileRequest, ShellResponse, FileResponse
10
+ )
11
+
12
+ # --- Endpoints ---
13
+
14
+ from src.application.session_manager import session_manager
15
+
16
+ @router.put("/sessions", response_model=StandardResponse)
17
+ async def create_session():
18
+ session = await session_manager.create_new_session()
19
+ return {"code": 0, "msg": "success", "data": {"session_id": session.session_id}}
20
+
21
+ @router.get("/sessions/{session_id}", response_model=StandardResponse)
22
+ async def get_session(session_id: str):
23
+ session = await session_manager.get_session_details(session_id)
24
+ if not session:
25
+ return {"code": 404, "msg": "Session not found", "data": None}
26
+ return {"code": 0, "msg": "success", "data": {"session_id": session.session_id, "title": session.title, "events": []}}
27
+
28
+ @router.get("/sessions", response_model=StandardResponse)
29
+ async def list_sessions():
30
+ sessions = await session_manager.list_all_sessions()
31
+ session_list = [
32
+ {
33
+ "session_id": s.session_id,
34
+ "title": s.title,
35
+ "latest_message": s.messages[-1].content if s.messages else "",
36
+ "latest_message_at": s.messages[-1].timestamp if s.messages else s.updated_at,
37
+ "status": s.status,
38
+ "unread_message_count": 0
39
+ }
40
+ for s in sessions
41
+ ]
42
+ return {"code": 0, "msg": "success", "data": {"sessions": session_list}}
43
+
44
+ @router.delete("/sessions/{session_id}", response_model=StandardResponse)
45
+ async def delete_session(session_id: str):
46
+ await session_manager.delete_session(session_id)
47
+ return {"code": 0, "msg": "success", "data": None}
48
+
49
+ @router.post("/sessions/{session_id}/stop", response_model=StandardResponse)
50
+ async def stop_session(session_id: str):
51
+ await session_manager.stop_session(session_id)
52
+ return {"code": 0, "msg": "success", "data": None}
53
+
54
+ from fastapi.responses import StreamingResponse
55
+ from src.application.chat_service import ChatService
56
+ from src.infrastructure.llm.gemini_client import GeminiAgent
57
+ import os
58
+ import json
59
+
60
+ # Initialize services (Dependency Injection would be better in production)
61
+ api_key = os.getenv("GEMINI_API_KEY")
62
+ gemini_agent = GeminiAgent(api_key=api_key)
63
+ chat_service = ChatService(session_manager, gemini_agent)
64
+
65
+ @router.post("/sessions/{session_id}/chat")
66
+ async def chat_session(session_id: str, request: ChatRequest):
67
+ async def event_generator():
68
+ async for event in chat_service.chat(session_id, request.message):
69
+ # Format as SSE
70
+ yield f"event: {event['event']}\ndata: {json.dumps(event['data'])}\n\n"
71
+
72
+ return StreamingResponse(event_generator(), media_type="text/event-stream")
73
+
74
+ from src.infrastructure.sandbox.docker_manager import docker_sandbox
75
+
76
+ @router.post("/sessions/{session_id}/shell", response_model=StandardResponse)
77
+ async def view_shell(session_id: str, request: ShellRequest):
78
+ if not docker_sandbox:
79
+ return {"code": 500, "msg": "Sandbox service unavailable", "data": None}
80
+
81
+ # In a real scenario, we might want to execute a command or get logs
82
+ # For this API endpoint "view shell session content", it implies getting history
83
+ # But the request body has "session_id" (shell session id).
84
+ # For simplicity, we'll just return a mock or the last command output if we tracked it.
85
+ # Let's assume we execute a 'whoami' just to prove it works, or return empty if no command provided.
86
+
87
+ # Since the API spec says "View shell session output", let's assume we are retrieving logs
88
+ # But our simple sandbox doesn't track shell sessions persistently yet.
89
+ # We will return a placeholder or execute a dummy command to verify connectivity.
90
+
91
+ output = await docker_sandbox.execute_shell(session_id, "echo 'Shell session active'")
92
+
93
+ return {
94
+ "code": 0,
95
+ "msg": "success",
96
+ "data": {
97
+ "output": output,
98
+ "session_id": request.session_id,
99
+ "console": [{"ps1": "$", "command": "echo 'Shell session active'", "output": output}]
100
+ }
101
+ }
102
+
103
+ @router.post("/sessions/{session_id}/file", response_model=StandardResponse)
104
+ async def view_file(session_id: str, request: FileRequest):
105
+ if not docker_sandbox:
106
+ return {"code": 500, "msg": "Sandbox service unavailable", "data": None}
107
+
108
+ content = await docker_sandbox.read_file(session_id, request.file)
109
+ return {"code": 0, "msg": "success", "data": {"content": content, "file": request.file}}
110
+
111
+ from fastapi import WebSocket, WebSocketDisconnect
112
+
113
+ @router.websocket("/sessions/{session_id}/vnc")
114
+ async def vnc_websocket(websocket: WebSocket, session_id: str):
115
+ await websocket.accept(subprotocol="binary")
116
+ try:
117
+ # In a real implementation, we would connect to the VNC port of the container
118
+ # and proxy the traffic.
119
+ # For now, we just keep the connection open and echo back any data or send a placeholder.
120
+ while True:
121
+ data = await websocket.receive_bytes()
122
+ # Echo or process
123
+ await websocket.send_bytes(data)
124
+ except WebSocketDisconnect:
125
+ print(f"VNC Client disconnected for session {session_id}")
126
+ except Exception as e:
127
+ print(f"VNC Error: {e}")
128
+ await websocket.close()