samiulttr commited on
Commit
71d3b6e
·
verified ·
1 Parent(s): ce10eaf

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +12 -9
  2. agents.py +75 -98
  3. all_sub_agents.py +24 -21
  4. app.py +290 -19
  5. requirements.txt +26 -8
  6. storage_paths.py +37 -0
Dockerfile CHANGED
@@ -19,15 +19,19 @@ RUN git clone --depth 1 https://github.com/github/github-mcp-server.git /tmp/git
19
  && rm -rf /tmp/github-mcp-server
20
  RUN npm install -g @zereight/mcp-gitlab
21
  RUN npm install -g maagpi-youtube-mcp
 
 
 
 
 
 
 
 
22
  # ── Non-root user (required by Hugging Face Spaces) ──────────────────────────
23
  RUN useradd -m -u 1000 user
24
  ENV HOME=/home/user \
25
  PATH=/home/user/.local/bin:$PATH
26
 
27
- # ── Install Playwright and Chromium ──────────────────────────────────────────
28
- RUN pip install --no-cache-dir playwright>=1.40.0 && \
29
- playwright install chromium && \
30
- playwright install-deps chromium
31
  # ── Working directory ─────────────────────────────────────────────────────────
32
  WORKDIR $HOME/app
33
 
@@ -36,19 +40,18 @@ COPY --chown=user requirements.txt .
36
  RUN pip install --no-cache-dir --upgrade pip \
37
  && pip install --no-cache-dir -r requirements.txt
38
 
39
-
40
  # ── Copy application code ─────────────────────────────────────────────────────
41
  COPY --chown=user . .
42
 
43
  # ── Switch to non-root user ───────────────────────────────────────────────────
44
  USER user
45
 
46
- # ── Expose status web UI port (HF Spaces default) ─────────────────────────────
47
  EXPOSE 7860
48
 
49
  # ── Healthcheck ────────────────────────────────────────────────────────────────
50
  HEALTHCHECK --interval=60s --timeout=10s --start-period=30s \
51
- CMD curl -f http://localhost:7860/health || exit 1
52
 
53
- # ── Launch bot ────────────────────────────────────────────────────────────────
54
- CMD ["python", "agents.py"]
 
19
  && rm -rf /tmp/github-mcp-server
20
  RUN npm install -g @zereight/mcp-gitlab
21
  RUN npm install -g maagpi-youtube-mcp
22
+
23
+ # ── Persistent memory directory shared by every agent ─────────────────────────
24
+ # NOTE: on Hugging Face Spaces, attach your Persistent Storage volume at this
25
+ # exact path (/agent). This mkdir/chmod is only a fallback for local/dev runs
26
+ # where no volume is mounted — once HF's storage bucket is mounted at /agent,
27
+ # its own permissions apply and this app writes only inside it.
28
+ RUN mkdir -p /agent && chmod -R 777 /agent
29
+
30
  # ── Non-root user (required by Hugging Face Spaces) ──────────────────────────
31
  RUN useradd -m -u 1000 user
32
  ENV HOME=/home/user \
33
  PATH=/home/user/.local/bin:$PATH
34
 
 
 
 
 
35
  # ── Working directory ─────────────────────────────────────────────────────────
36
  WORKDIR $HOME/app
37
 
 
40
  RUN pip install --no-cache-dir --upgrade pip \
41
  && pip install --no-cache-dir -r requirements.txt
42
 
 
43
  # ── Copy application code ─────────────────────────────────────────────────────
44
  COPY --chown=user . .
45
 
46
  # ── Switch to non-root user ───────────────────────────────────────────────────
47
  USER user
48
 
49
+ # ── Expose web UI port (HF Spaces default) ─────────────────────────────────────
50
  EXPOSE 7860
51
 
52
  # ── Healthcheck ────────────────────────────────────────────────────────────────
53
  HEALTHCHECK --interval=60s --timeout=10s --start-period=30s \
54
+ CMD curl -f http://localhost:7860/ || exit 1
55
 
56
+ # ── Launch app (FastAPI + Manus-style UI) ───────────────────────────────────────
57
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
agents.py CHANGED
@@ -1,57 +1,48 @@
1
  import os
2
  from dotenv import load_dotenv
3
- from crewai import Agent, Task, Crew, Process,LLM,Memory
4
- from crewai_tools import SerperDevTool
5
- from crewai_files import File, FileBytes
6
- from all_sub_agents import ALL_SUB_AGENTS
 
 
7
 
8
  load_dotenv()
9
 
 
 
 
 
 
 
10
 
11
- # ---------------------------------------------------------------------------# MAIN AGENT LLM
12
  # ---------------------------------------------------------------------------
13
- _llm=LLM(
 
 
 
14
  api_key=os.getenv("LLM_API_KEY"),
15
- model=os.getenv("LLM_MODEL")
16
-
17
- )
18
- lmmn=os.getenv("LLM_API_KEY")
19
- _memory = Memory(
20
- embedder={
21
- "provider": "google-generativeai",
22
- "config": {
23
- "model_name": "gemini-embedding-001",
24
- "api_key": f"{lmmn}",
25
- },
26
- },
27
- llm="google/gemini-embedding-001",
28
- storage="/agent/memory/",
29
- recency_weight=0.6,
30
- semantic_weight=0.4,
31
- importance_weight=0.3,
32
- recency_half_life_days=7,
33
-
34
-
35
-
36
  )
37
 
38
  # ---------------------------------------------------------------------------
39
- #ASSISTANT AGENT BACKSTORY AND GOAL
40
  # ---------------------------------------------------------------------------
41
- Goal=(
42
  "Carefully understand whatever the user asks for - a question, an "
43
  "instruction, or an attached file - and complete that exact task with "
44
  "maximum accuracy, depth, and relevance, whether it involves research, "
45
  "writing, analysis, problem-solving, planning, or any other kind of "
46
  "intellectual or administrative work, so the user can rely on a single, "
47
- "highly capable, completely trustworthy assistant for anything they need."
48
- "You were made by Samuel.And I don't like Samiul's lying, false promises, false accusations. In a word, I don't like all lies and hallucinations."
49
- "So always give the right answer and tell the truth. If you don't know or can't, then clearly state the problem."
 
 
50
  "মোঃ ছামিউল ইসলাম (MD SAMIUL ISLAM) এর পার্সোনাল এজেন্ট এবং এসিস্ট্যান্ট "
51
  "ছামিউল আমাকে তৈরি করেছে"
52
  )
53
 
54
- Backstory=(
55
  "You are a versatile, deeply experienced assistant who has spent years "
56
  "working across research, analysis, writing, technology, business, and "
57
  "creative fields. Your thinking is structured, your analysis is sharp, "
@@ -59,78 +50,64 @@ Backstory=(
59
  "instruction before acting. Whatever the user asks, you handle it with "
60
  "patience, honesty, and complete care. You are not just a tool - you are "
61
  "the user's most trusted, sharpest, and most reliable partner, someone "
62
- "who can be handed any task without hesitation and who never lets them down."
63
- "When delegating tasks that require real GitHub actions, always instruct the coworker to execute the action via their tools and return the tool's actual output — never accept a 'guide' or 'instructions' as a substitute for the real action."
64
- "Use the snake agents you have to make Samuel work for the one who created you."
65
- "When you are asked to do something, you search your entire system to see if anyone can do the job. If they can, get it done."
66
- "The agents you have have some special features, use them to complete the task, they have tools or MCP is added so they can use them and work with them if they want."
67
- "If you are Samiul's main agent, then when Samiul asks you to work, you will do his work. You don't need to use your own tools because Samiul has put a snake agent under you to work."
 
 
 
 
68
  "আমি ছামিউল এর তৈরি একটা ভার্চুয়াল রোবট বা এআই এসিস্ট্যান্ট এজেন্ট"
69
  )
70
 
 
71
 
72
  # ---------------------------------------------------------------------------
73
- # 1) Main Agent - the personal, trusted, all-purpose assistant
74
- # ---------------------------------------------------------------------------
75
- main_assistant_agent= Agent(
76
- role="Chief Personal Assistant",
77
- goal=Goal,
78
- backstory=Backstory,
79
- llm=_llm,
80
- inject_date=True,
81
- verbose=True,
82
- allow_delegation=True,
83
- max_iter=10,
84
- max_retry_limit=6,
85
- respect_context_window=False,
86
- use_system_prompt=False,
87
- multimodal=False,
88
- reasoning=False,
89
- #max_reasoning_attempts=4,
90
- memory=_memory,
91
- )
92
- # ---------------------------------------------------------------------------
93
- #MAIN ASSISTANT AGENTING SYSTEM
94
  # ---------------------------------------------------------------------------
95
- def main_agent(user_command: str,user_attachment: str | None = None) -> str:
96
-
97
- attachment_files = {}
98
-
99
- if user_attachment is not None:
100
- attachment_files["attached_file"] = File(source=user_attachment)
101
- # ---------------------------------------------------------------------------
102
- #MAIN TASK
103
- # ---------------------------------------------------------------------------
104
- main_task = Task(
105
- description=user_command,
106
- expected_output=(
107
- "A complete, clear, accurate, and directly usable result for whatever "
108
- "task is described in the instruction."
109
- ),
110
- input_files=attachment_files,
111
- )
112
 
 
 
 
 
 
 
113
 
114
- # ---------------------------------------------------------------------------
115
- #MAIN CREW
116
- # ---------------------------------------------------------------------------
117
- main_crew = Crew(
118
- agents=ALL_SUB_AGENTS,
119
- manager_agent=main_assistant_agent,
120
- tasks=[main_task],
121
- process=Process.hierarchical,
122
- verbose=True,
123
 
 
 
 
 
 
 
 
 
 
124
  )
125
-
126
- # ---------------------------------------------------------------------------
127
- #MAIN KICKOFF INPUT AND OUTPUT SYSTEM
128
- # ---------------------------------------------------------------------------
129
- return str(main_crew.kickoff())
130
-
131
-
132
-
133
- if __name__ == "__main__":
134
- from app import chat_agen
135
- chat_agent()
136
-
 
 
 
 
 
 
 
 
1
  import os
2
  from dotenv import load_dotenv
3
+ from langchain.chat_models import init_chat_model
4
+ from langgraph_supervisor import create_supervisor
5
+ from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
6
+
7
+ from all_sub_agents import build_all_sub_agents
8
+ from storage_paths import agent_dir
9
 
10
  load_dotenv()
11
 
12
+ # ---------------------------------------------------------------------------
13
+ # AGENT IDENTITY / MEMORY LOCATION -> /agent/main_agent/ (persistent bucket)
14
+ # ---------------------------------------------------------------------------
15
+ AGENT_NAME = "main_agent"
16
+ MEMORY_DIR = agent_dir(AGENT_NAME)
17
+ DB_PATH = os.path.join(MEMORY_DIR, "state.db")
18
 
 
19
  # ---------------------------------------------------------------------------
20
+ # MAIN AGENT LLM
21
+ # ---------------------------------------------------------------------------
22
+ _llm = init_chat_model(
23
+ model=os.getenv("LLM_MODEL"),
24
  api_key=os.getenv("LLM_API_KEY"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  )
26
 
27
  # ---------------------------------------------------------------------------
28
+ # ASSISTANT AGENT BACKSTORY AND GOAL
29
  # ---------------------------------------------------------------------------
30
+ GOAL = (
31
  "Carefully understand whatever the user asks for - a question, an "
32
  "instruction, or an attached file - and complete that exact task with "
33
  "maximum accuracy, depth, and relevance, whether it involves research, "
34
  "writing, analysis, problem-solving, planning, or any other kind of "
35
  "intellectual or administrative work, so the user can rely on a single, "
36
+ "highly capable, completely trustworthy assistant for anything they need. "
37
+ "You were made by Samuel. And I don't like Samiul's lying, false promises, "
38
+ "false accusations. In a word, I don't like all lies and hallucinations. "
39
+ "So always give the right answer and tell the truth. If you don't know or "
40
+ "can't, then clearly state the problem. "
41
  "মোঃ ছামিউল ইসলাম (MD SAMIUL ISLAM) এর পার্সোনাল এজেন্ট এবং এসিস্ট্যান্ট "
42
  "ছামিউল আমাকে তৈরি করেছে"
43
  )
44
 
45
+ BACKSTORY = (
46
  "You are a versatile, deeply experienced assistant who has spent years "
47
  "working across research, analysis, writing, technology, business, and "
48
  "creative fields. Your thinking is structured, your analysis is sharp, "
 
50
  "instruction before acting. Whatever the user asks, you handle it with "
51
  "patience, honesty, and complete care. You are not just a tool - you are "
52
  "the user's most trusted, sharpest, and most reliable partner, someone "
53
+ "who can be handed any task without hesitation and who never lets them "
54
+ "down. When delegating tasks that require real actions (GitHub, GitLab, "
55
+ "Facebook, YouTube), always instruct the sub-agent to execute the action "
56
+ "via their tools and return the tool's actual output - never accept a "
57
+ "'guide' or 'instructions' as a substitute for the real action. Use the "
58
+ "sub-agents you have to get Samiul's work done. When you are asked to do "
59
+ "something, check whether one of your sub-agents can do the job, and if "
60
+ "so, delegate it to them and report back their real result. If the task "
61
+ "is general knowledge, writing, analysis, or anything that does not need "
62
+ "a specific platform, handle it yourself directly instead of delegating. "
63
  "আমি ছামিউল এর তৈরি একটা ভার্চুয়াল রোবট বা এআই এসিস্ট্যান্ট এজেন্ট"
64
  )
65
 
66
+ SUPERVISOR_PROMPT = GOAL + "\n\n" + BACKSTORY
67
 
68
  # ---------------------------------------------------------------------------
69
+ # GLOBAL, LAZILY-BUILT SINGLETON (built once at app startup, reused per request)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  # ---------------------------------------------------------------------------
71
+ _main_graph = None
72
+ _all_checkpointer_cms = []
73
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
+ async def get_main_agent():
76
+ """
77
+ Returns the compiled main (supervisor) LangGraph agent, building it
78
+ (and every sub-agent + their persistent memories) on first call.
79
+ """
80
+ global _main_graph, _all_checkpointer_cms
81
 
82
+ if _main_graph is not None:
83
+ return _main_graph
 
 
 
 
 
 
 
84
 
85
+ sub_agents, sub_cms = await build_all_sub_agents()
86
+
87
+ supervisor_builder = create_supervisor(
88
+ agents=sub_agents,
89
+ model=_llm,
90
+ prompt=SUPERVISOR_PROMPT,
91
+ supervisor_name=AGENT_NAME,
92
+ add_handoff_back_messages=True,
93
+ output_mode="full_history",
94
  )
95
+
96
+ saver_cm = AsyncSqliteSaver.from_conn_string(DB_PATH)
97
+ checkpointer = await saver_cm.__aenter__()
98
+
99
+ _main_graph = supervisor_builder.compile(checkpointer=checkpointer, name=AGENT_NAME)
100
+ _all_checkpointer_cms = sub_cms + [saver_cm]
101
+
102
+ return _main_graph
103
+
104
+
105
+ async def close_main_agent():
106
+ """Call on app shutdown to cleanly close every agent's sqlite connection."""
107
+ global _all_checkpointer_cms
108
+ for cm in _all_checkpointer_cms:
109
+ try:
110
+ await cm.__aexit__(None, None, None)
111
+ except Exception:
112
+ pass
113
+ _all_checkpointer_cms = []
all_sub_agents.py CHANGED
@@ -1,26 +1,29 @@
1
  # ---------------------------------------------------------------------------
2
- #IMPORT_ALL_SUB_AGENTS
3
  # ---------------------------------------------------------------------------
4
- from sub_gents.coding.git_hub_agent.git_hub import _git_hub_agent
5
- from sub_gents.coding.git_lab_agent.git_lab import _git_lab_agent
6
- from sub_gents.social_media.Facebook_agent.facebook import _facebook_agent
7
- from sub_gents.social_media.youtube_agent.youtube import _youtube_agent
8
 
9
- # ---------------------------------------------------------------------------
10
- #CREATE_OBJECT_ALL_SUB_AGENTS
11
- # ---------------------------------------------------------------------------
12
- _git_hub=_git_hub_agent()
13
- _git_lab=_git_lab_agent()
14
- _facebook=_facebook_agent()
15
- _youtube=_youtube_agent()
16
 
17
 
18
- # ---------------------------------------------------------------------------
19
- #ALL_SUB_AGENTS_LIST
20
- # ---------------------------------------------------------------------------
21
- ALL_SUB_AGENTS=[
22
- _git_hub,
23
- _git_lab,
24
- _facebook,
25
- _youtube,
26
- ]
 
 
 
 
 
 
 
 
 
 
1
  # ---------------------------------------------------------------------------
2
+ # IMPORT_ALL_SUB_AGENT_BUILDERS
3
  # ---------------------------------------------------------------------------
4
+ from sub_agents.git_hub_agent.git_hub import build_git_hub_agent, AGENT_NAME as GITHUB_NAME
5
+ from sub_agents.git_lab_agent.git_lab import build_git_lab_agent, AGENT_NAME as GITLAB_NAME
6
+ from sub_agents.facebook_agent.facebook import build_facebook_agent, AGENT_NAME as FACEBOOK_NAME
7
+ from sub_agents.youtube_agent.youtube import build_youtube_agent, AGENT_NAME as YOUTUBE_NAME
8
 
9
+ SUB_AGENT_NAMES = [GITHUB_NAME, GITLAB_NAME, FACEBOOK_NAME, YOUTUBE_NAME]
 
 
 
 
 
 
10
 
11
 
12
+ async def build_all_sub_agents():
13
+ """
14
+ Builds every sub agent (each with its own compiled LangGraph react-agent
15
+ and its own persistent sqlite memory under /agent/<agent_name>/).
16
+
17
+ Returns:
18
+ agents: list[CompiledStateGraph] -> handed to the supervisor
19
+ checkpointer_cms: list -> async context managers to close on shutdown
20
+ """
21
+ git_hub_agent, gh_cm = await build_git_hub_agent()
22
+ git_lab_agent, gl_cm = await build_git_lab_agent()
23
+ facebook_agent, fb_cm = await build_facebook_agent()
24
+ youtube_agent, yt_cm = await build_youtube_agent()
25
+
26
+ agents = [git_hub_agent, git_lab_agent, facebook_agent, youtube_agent]
27
+ checkpointer_cms = [gh_cm, gl_cm, fb_cm, yt_cm]
28
+
29
+ return agents, checkpointer_cms
app.py CHANGED
@@ -1,28 +1,299 @@
1
- import gradio as gr
 
 
 
 
 
2
 
3
- from agents import main_agent
 
 
 
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- def chat_fn(message, history):
7
- """
8
- Gradio multimodal ChatInterface callback.
9
 
10
- `message` is a dict: {"text": str, "files": [local_path, ...]}
11
- when multimodal=True. We forward the text and (at most) the first
12
- attached file straight into main_agent(user_command=, user_attachment=).
 
 
 
 
 
 
13
  """
14
- text = message.get("text", "") if isinstance(message, dict) else str(message)
15
- files = message.get("files", []) if isinstance(message, dict) else []
16
- attachment = files[0] if files else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- return main_agent(user_command=text, user_attachment=attachment)
 
 
19
 
 
20
 
21
- demo = gr.ChatInterface(
22
- fn=chat_fn,
23
- multimodal=True,
24
- title="Personal Assistant(owner name:MD SAMIUL ISLAM)",
25
- description="Chat with your assistant. You can attach a file with your message.Currently, this platform is not working properly due to the limitations of the platform and the lack of tokens for the LLM model. ",
26
- )
27
 
28
- chat_agent=demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
1
+ import os
2
+ import json
3
+ import uuid
4
+ import asyncio
5
+ import shutil
6
+ from datetime import datetime, timezone
7
 
8
+ from fastapi import FastAPI, UploadFile, File, Form
9
+ from fastapi.responses import StreamingResponse, JSONResponse
10
+ from fastapi.staticfiles import StaticFiles
11
+ from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
12
 
13
+ from agents import get_main_agent, close_main_agent, AGENT_NAME
14
+ from all_sub_agents import SUB_AGENT_NAMES
15
+ from storage_paths import agent_dir
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # PERSISTENCE PATHS — everything (thread index + uploaded files) lives inside
19
+ # the persistent /agent storage bucket, under this agent's own folder, so a
20
+ # Space restart/redeploy never loses conversation history or attachments.
21
+ # ---------------------------------------------------------------------------
22
+ MAIN_MEMORY_DIR = agent_dir(AGENT_NAME)
23
+ THREADS_INDEX_PATH = os.path.join(MAIN_MEMORY_DIR, "threads.json")
24
+ UPLOADS_DIR = os.path.join(MAIN_MEMORY_DIR, "uploads")
25
+ os.makedirs(UPLOADS_DIR, exist_ok=True)
26
+
27
+ _threads_lock = asyncio.Lock()
28
+
29
+ app = FastAPI(title="Personal Assistant")
30
+ app.mount("/static", StaticFiles(directory="static"), name="static")
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # THREAD INDEX HELPERS (so the sidebar / history survive a page refresh)
35
+ # ---------------------------------------------------------------------------
36
+ def _read_threads():
37
+ if not os.path.exists(THREADS_INDEX_PATH):
38
+ return []
39
+ try:
40
+ with open(THREADS_INDEX_PATH, "r", encoding="utf-8") as f:
41
+ return json.load(f)
42
+ except Exception:
43
+ return []
44
+
45
+
46
+ def _write_threads(threads):
47
+ with open(THREADS_INDEX_PATH, "w", encoding="utf-8") as f:
48
+ json.dump(threads, f, ensure_ascii=False, indent=2)
49
+
50
+
51
+ async def _touch_thread(thread_id: str, title: str | None = None):
52
+ async with _threads_lock:
53
+ threads = _read_threads()
54
+ now = datetime.now(timezone.utc).isoformat()
55
+ found = None
56
+ for t in threads:
57
+ if t["id"] == thread_id:
58
+ found = t
59
+ break
60
+ if found is None:
61
+ found = {
62
+ "id": thread_id,
63
+ "title": title or "নতুন কথোপকথন",
64
+ "created_at": now,
65
+ "updated_at": now,
66
+ }
67
+ threads.insert(0, found)
68
+ else:
69
+ found["updated_at"] = now
70
+ if title and found.get("title") == "নতুন কথোপকথন":
71
+ found["title"] = title
72
+ threads.remove(found)
73
+ threads.insert(0, found)
74
+ _write_threads(threads)
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # STARTUP / SHUTDOWN
79
+ # ---------------------------------------------------------------------------
80
+ @app.on_event("startup")
81
+ async def _startup():
82
+ await get_main_agent()
83
+
84
+
85
+ @app.on_event("shutdown")
86
+ async def _shutdown():
87
+ await close_main_agent()
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # API: index page
92
+ # ---------------------------------------------------------------------------
93
+ @app.get("/")
94
+ async def index():
95
+ from fastapi.responses import FileResponse
96
+ return FileResponse("static/index.html")
97
+
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # API: list conversation threads (sidebar)
101
+ # ---------------------------------------------------------------------------
102
+ @app.get("/api/threads")
103
+ async def list_threads():
104
+ return JSONResponse(_read_threads())
105
+
106
+
107
+ @app.post("/api/threads")
108
+ async def create_thread():
109
+ thread_id = str(uuid.uuid4())
110
+ await _touch_thread(thread_id)
111
+ return JSONResponse({"thread_id": thread_id})
112
+
113
+
114
+ @app.delete("/api/threads/{thread_id}")
115
+ async def delete_thread(thread_id: str):
116
+ async with _threads_lock:
117
+ threads = [t for t in _read_threads() if t["id"] != thread_id]
118
+ _write_threads(threads)
119
+ return JSONResponse({"ok": True})
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # API: file upload (attachment)
124
+ # ---------------------------------------------------------------------------
125
+ @app.post("/api/upload")
126
+ async def upload_file(file: UploadFile = File(...)):
127
+ safe_name = f"{uuid.uuid4().hex}_{file.filename}"
128
+ dest_path = os.path.join(UPLOADS_DIR, safe_name)
129
+ with open(dest_path, "wb") as f:
130
+ shutil.copyfileobj(file.file, f)
131
+ return JSONResponse({"path": dest_path, "filename": file.filename})
132
+
133
+
134
+ # ---------------------------------------------------------------------------
135
+ # HELPERS: turn LangGraph's saved message state into a UI-friendly transcript
136
+ # ---------------------------------------------------------------------------
137
+ def _stringify(value) -> str:
138
+ try:
139
+ if isinstance(value, (dict, list)):
140
+ return json.dumps(value, ensure_ascii=False, default=str)[:4000]
141
+ return str(value)[:4000]
142
+ except Exception:
143
+ return str(value)[:4000]
144
 
 
 
 
145
 
146
+ def _agent_from_tool_name(tool_name: str) -> str | None:
147
+ if tool_name and tool_name.startswith("transfer_to_"):
148
+ candidate = tool_name[len("transfer_to_"):]
149
+ if candidate in SUB_AGENT_NAMES:
150
+ return candidate
151
+ return None
152
+
153
+
154
+ def _messages_to_turns(messages):
155
  """
156
+ Walk a flattened LangGraph message history (as produced by
157
+ langgraph_supervisor with output_mode='full_history') and rebuild
158
+ Manus-style turns: {role: user, text} and
159
+ {role: assistant, steps: [...], text: final_answer}.
160
+ """
161
+ turns = []
162
+ current = None
163
+ pending_tool_calls = {} # tool_call_id -> step dict
164
+
165
+ def _new_assistant_turn():
166
+ return {"role": "assistant", "steps": [], "text": ""}
167
+
168
+ for msg in messages:
169
+ if isinstance(msg, HumanMessage):
170
+ turns.append({"role": "user", "text": msg.content if isinstance(msg.content, str) else _stringify(msg.content)})
171
+ current = _new_assistant_turn()
172
+ turns.append(current)
173
+
174
+ elif isinstance(msg, AIMessage):
175
+ if current is None:
176
+ current = _new_assistant_turn()
177
+ turns.append(current)
178
+
179
+ tool_calls = getattr(msg, "tool_calls", None) or []
180
+ if tool_calls:
181
+ for tc in tool_calls:
182
+ tool_name = tc.get("name")
183
+ agent_name = _agent_from_tool_name(tool_name)
184
+ if agent_name:
185
+ step = {"type": "agent_start", "agent": agent_name}
186
+ else:
187
+ step = {
188
+ "type": "tool",
189
+ "tool": tool_name,
190
+ "input": _stringify(tc.get("args")),
191
+ "output": None,
192
+ }
193
+ current["steps"].append(step)
194
+ pending_tool_calls[tc.get("id")] = step
195
+ elif msg.content:
196
+ text = msg.content if isinstance(msg.content, str) else _stringify(msg.content)
197
+ if text.strip():
198
+ current["text"] = text
199
+
200
+ elif isinstance(msg, ToolMessage):
201
+ step = pending_tool_calls.get(msg.tool_call_id)
202
+ if step is not None and step.get("type") == "tool":
203
+ step["output"] = _stringify(msg.content)
204
+
205
+ return turns
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # API: load full history for a thread (used on page refresh)
210
+ # ---------------------------------------------------------------------------
211
+ @app.get("/api/history")
212
+ async def history(thread_id: str):
213
+ graph = await get_main_agent()
214
+ config = {"configurable": {"thread_id": thread_id}}
215
+ try:
216
+ state = await graph.aget_state(config)
217
+ except Exception:
218
+ return JSONResponse({"turns": []})
219
+
220
+ if not state or not state.values:
221
+ return JSONResponse({"turns": []})
222
+
223
+ messages = state.values.get("messages", [])
224
+ turns = _messages_to_turns(messages)
225
+ return JSONResponse({"turns": turns})
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # API: streaming chat endpoint (NDJSON stream of Manus-style events)
230
+ # ---------------------------------------------------------------------------
231
+ def _sse(obj: dict) -> str:
232
+ return json.dumps(obj, ensure_ascii=False) + "\n"
233
+
234
+
235
+ @app.post("/api/chat")
236
+ async def chat(
237
+ thread_id: str = Form(...),
238
+ text: str = Form(""),
239
+ attachment_path: str | None = Form(None),
240
+ ):
241
+ graph = await get_main_agent()
242
+ config = {"configurable": {"thread_id": thread_id}}
243
+
244
+ user_text = text or ""
245
+ if attachment_path:
246
+ user_text = f"{user_text}\n\n[সংযুক্ত ফাইল: {attachment_path}]"
247
+
248
+ await _touch_thread(thread_id, title=(text or "নতুন কথোপকথন")[:60])
249
+
250
+ inputs = {"messages": [HumanMessage(content=user_text)]}
251
+
252
+ async def event_stream():
253
+ try:
254
+ async for event in graph.astream_events(inputs, config=config, version="v2"):
255
+ kind = event.get("event")
256
+ node = (event.get("metadata") or {}).get("langgraph_node")
257
+ name = event.get("name")
258
+
259
+ if kind == "on_chain_start" and name in SUB_AGENT_NAMES and node == name:
260
+ yield _sse({"type": "agent_start", "agent": name})
261
+
262
+ elif kind == "on_tool_start":
263
+ yield _sse({
264
+ "type": "tool_start",
265
+ "agent": node,
266
+ "tool": name,
267
+ "input": _stringify((event.get("data") or {}).get("input")),
268
+ })
269
+
270
+ elif kind == "on_tool_end":
271
+ output = (event.get("data") or {}).get("output")
272
+ yield _sse({
273
+ "type": "tool_end",
274
+ "agent": node,
275
+ "tool": name,
276
+ "output": _stringify(output),
277
+ })
278
+
279
+ elif kind == "on_chat_model_stream" and node == AGENT_NAME:
280
+ chunk = (event.get("data") or {}).get("chunk")
281
+ text_piece = getattr(chunk, "content", "") if chunk else ""
282
+ if isinstance(text_piece, list):
283
+ text_piece = "".join(
284
+ part.get("text", "") if isinstance(part, dict) else str(part)
285
+ for part in text_piece
286
+ )
287
+ if text_piece:
288
+ yield _sse({"type": "token", "text": text_piece})
289
 
290
+ yield _sse({"type": "done"})
291
+ except Exception as exc: # noqa: BLE001
292
+ yield _sse({"type": "error", "message": str(exc)})
293
 
294
+ return StreamingResponse(event_stream(), media_type="application/x-ndjson")
295
 
 
 
 
 
 
 
296
 
297
+ if __name__ == "__main__":
298
+ import uvicorn
299
+ uvicorn.run("app:app", host="0.0.0.0", port=7860)
requirements.txt CHANGED
@@ -1,10 +1,28 @@
1
- gradio>=4.44.0
2
- crewai[file-processing,google-genai,mcp]==1.15.9
3
- crewai-tools[mcp]==1.15.9
4
- google-generativeai
 
 
 
 
 
5
  mcp
6
- uv
7
- just-facebook-mcp
8
- langchain_community
9
- PyGithub
 
 
 
 
 
 
 
 
 
 
 
 
10
  python-dotenv
 
 
1
+ # ---------------------------------------------------------------------------
2
+ # Core agent framework (as requested)
3
+ # ---------------------------------------------------------------------------
4
+ langgraph==1.2.11
5
+ langchain==1.3.15
6
+ langgraph-supervisor
7
+ langgraph-checkpoint-sqlite
8
+ langchain-mcp-adapters
9
+ langchain-core
10
  mcp
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # LLM providers used by init_chat_model (add/remove based on which
14
+ # LLM_MODEL / SUB_LLM_MODEL you actually use, e.g. "openai:gpt-4.1",
15
+ # "google_genai:gemini-2.0-flash", "anthropic:claude-...")
16
+ # ---------------------------------------------------------------------------
17
+ langchain-openai
18
+ langchain-google-genai
19
+ langchain-anthropic
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Web server / UI
23
+ # ---------------------------------------------------------------------------
24
+ fastapi
25
+ uvicorn[standard]
26
+ python-multipart
27
  python-dotenv
28
+ aiosqlite
storage_paths.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # ---------------------------------------------------------------------------
4
+ # /agent is expected to be a PERSISTENT storage bucket (mounted with write
5
+ # permission) — e.g. on Hugging Face Spaces, attach your Persistent Storage
6
+ # volume at this exact path. Everything any agent needs to remember
7
+ # (sqlite state, thread index, uploaded files) is written ONLY under here,
8
+ # so it survives Space restarts/redeploys instead of living on the
9
+ # ephemeral container filesystem.
10
+ # ---------------------------------------------------------------------------
11
+ AGENT_ROOT = "/agent"
12
+
13
+
14
+ def agent_dir(agent_name: str) -> str:
15
+ """
16
+ Returns (and creates if needed) /agent/<agent_name>/, and fails loudly
17
+ with a clear message if that location isn't actually writable — instead
18
+ of silently falling back to ephemeral local storage.
19
+ """
20
+ path = os.path.join(AGENT_ROOT, agent_name)
21
+ os.makedirs(path, exist_ok=True)
22
+
23
+ probe = os.path.join(path, ".write_test")
24
+ try:
25
+ with open(probe, "w") as f:
26
+ f.write("ok")
27
+ os.remove(probe)
28
+ except OSError as exc:
29
+ raise RuntimeError(
30
+ f"'{path}' পাথে write access পাওয়া যায়নি। এই অ্যাপের সব agent-এর "
31
+ f"memory/state '/agent' নামের একটি persistent storage বাকেটে রাখা হয় — "
32
+ f"HF Space-এ Persistent Storage সেই '/agent' পাথে সঠিকভাবে মাউন্ট করা "
33
+ f"আছে কিনা এবং তাতে write permission আছে কিনা যাচাই করুন. "
34
+ f"(আসল এরর: {exc})"
35
+ ) from exc
36
+
37
+ return path