Spaces:
Runtime error
Runtime error
Upload 6 files
Browse files- .env.example +38 -0
- agents.py +50 -45
- all_sub_agents.py +29 -23
- app.py +14 -19
- storage_paths.py +56 -4
.env.example
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ---------------------------------------------------------------------------
|
| 2 |
+
# MAIN AGENT (supervisor) LLM
|
| 3 |
+
# model string format for langchain's init_chat_model, e.g.:
|
| 4 |
+
# openai:gpt-4.1 | google_genai:gemini-2.0-flash | anthropic:claude-sonnet-4-5
|
| 5 |
+
# ---------------------------------------------------------------------------
|
| 6 |
+
LLM_API_KEY=
|
| 7 |
+
LLM_MODEL=openai:gpt-4.1
|
| 8 |
+
|
| 9 |
+
# ---------------------------------------------------------------------------
|
| 10 |
+
# SUB AGENTS LLM (GitHub / GitLab / Facebook / YouTube)
|
| 11 |
+
# ---------------------------------------------------------------------------
|
| 12 |
+
SUB_LLM_API_KEY=
|
| 13 |
+
SUB_LLM_MODEL=openai:gpt-4.1-mini
|
| 14 |
+
|
| 15 |
+
# ---------------------------------------------------------------------------
|
| 16 |
+
# GitHub
|
| 17 |
+
# ---------------------------------------------------------------------------
|
| 18 |
+
GITHUB_PAT=
|
| 19 |
+
|
| 20 |
+
# ---------------------------------------------------------------------------
|
| 21 |
+
# GitLab
|
| 22 |
+
# ---------------------------------------------------------------------------
|
| 23 |
+
GITLAB_PAT=
|
| 24 |
+
GITLAB_API_URL=https://gitlab.com/api/v4
|
| 25 |
+
GITLAB_READ_ONLY_MODE=false
|
| 26 |
+
|
| 27 |
+
# ---------------------------------------------------------------------------
|
| 28 |
+
# Facebook
|
| 29 |
+
# ---------------------------------------------------------------------------
|
| 30 |
+
FACEBOOK_PAT=
|
| 31 |
+
FACEBOOK_PID=
|
| 32 |
+
|
| 33 |
+
# ---------------------------------------------------------------------------
|
| 34 |
+
# YouTube
|
| 35 |
+
# ---------------------------------------------------------------------------
|
| 36 |
+
YOUTUBE_CID=
|
| 37 |
+
YOUTUBE_CLIENT_SECRET=
|
| 38 |
+
YOUTUBE_MCP_TRANSPORT=stdio
|
agents.py
CHANGED
|
@@ -2,10 +2,9 @@ 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
|
| 8 |
-
from storage_paths import agent_dir
|
| 9 |
|
| 10 |
load_dotenv()
|
| 11 |
|
|
@@ -13,6 +12,7 @@ load_dotenv()
|
|
| 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 |
|
|
@@ -24,10 +24,16 @@ _llm = init_chat_model(
|
|
| 24 |
api_key=os.getenv("LLM_API_KEY"),
|
| 25 |
)
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
# ---------------------------------------------------------------------------
|
| 28 |
# ASSISTANT AGENT BACKSTORY AND GOAL
|
| 29 |
# ---------------------------------------------------------------------------
|
| 30 |
-
|
| 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, "
|
|
@@ -42,7 +48,7 @@ GOAL = (
|
|
| 42 |
"ছামিউল আমাকে তৈরি করেছে"
|
| 43 |
)
|
| 44 |
|
| 45 |
-
|
| 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, "
|
|
@@ -63,51 +69,50 @@ BACKSTORY = (
|
|
| 63 |
"আমি ছামিউল এর তৈরি একটা ভার্চুয়াল রোবট বা এআই এসিস্ট্যান্ট এজেন্ট"
|
| 64 |
)
|
| 65 |
|
| 66 |
-
SUPERVISOR_PROMPT =
|
| 67 |
|
| 68 |
# ---------------------------------------------------------------------------
|
| 69 |
-
#
|
|
|
|
| 70 |
# ---------------------------------------------------------------------------
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
"""
|
| 77 |
-
|
| 78 |
-
(
|
|
|
|
|
|
|
|
|
|
| 79 |
"""
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 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 |
-
|
| 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 = []
|
|
|
|
| 2 |
from dotenv import load_dotenv
|
| 3 |
from langchain.chat_models import init_chat_model
|
| 4 |
from langgraph_supervisor import create_supervisor
|
|
|
|
| 5 |
|
| 6 |
+
from all_sub_agents import ALL_SUB_AGENTS
|
| 7 |
+
from storage_paths import agent_dir, open_agent_sqlite
|
| 8 |
|
| 9 |
load_dotenv()
|
| 10 |
|
|
|
|
| 12 |
# AGENT IDENTITY / MEMORY LOCATION -> /agent/main_agent/ (persistent bucket)
|
| 13 |
# ---------------------------------------------------------------------------
|
| 14 |
AGENT_NAME = "main_agent"
|
| 15 |
+
ROLE = "Chief Personal Assistant"
|
| 16 |
MEMORY_DIR = agent_dir(AGENT_NAME)
|
| 17 |
DB_PATH = os.path.join(MEMORY_DIR, "state.db")
|
| 18 |
|
|
|
|
| 24 |
api_key=os.getenv("LLM_API_KEY"),
|
| 25 |
)
|
| 26 |
|
| 27 |
+
# ---------------------------------------------------------------------------
|
| 28 |
+
# STATE / MEMORY (sqlite, kept for this agent's whole lifetime, tuned to be
|
| 29 |
+
# safe on S3-style / object-storage persistent buckets — see storage_paths.py)
|
| 30 |
+
# ---------------------------------------------------------------------------
|
| 31 |
+
_checkpointer = open_agent_sqlite(DB_PATH)
|
| 32 |
+
|
| 33 |
# ---------------------------------------------------------------------------
|
| 34 |
# ASSISTANT AGENT BACKSTORY AND GOAL
|
| 35 |
# ---------------------------------------------------------------------------
|
| 36 |
+
Goal = (
|
| 37 |
"Carefully understand whatever the user asks for - a question, an "
|
| 38 |
"instruction, or an attached file - and complete that exact task with "
|
| 39 |
"maximum accuracy, depth, and relevance, whether it involves research, "
|
|
|
|
| 48 |
"ছামিউল আমাকে তৈরি করেছে"
|
| 49 |
)
|
| 50 |
|
| 51 |
+
Backstory = (
|
| 52 |
"You are a versatile, deeply experienced assistant who has spent years "
|
| 53 |
"working across research, analysis, writing, technology, business, and "
|
| 54 |
"creative fields. Your thinking is structured, your analysis is sharp, "
|
|
|
|
| 69 |
"আমি ছামিউল এর তৈরি একটা ভার্চুয়াল রোবট বা এআই এসিস্ট্যান্ট এজেন্ট"
|
| 70 |
)
|
| 71 |
|
| 72 |
+
SUPERVISOR_PROMPT = f"You are the {ROLE}.\n\n" + Goal + "\n\n" + Backstory
|
| 73 |
|
| 74 |
# ---------------------------------------------------------------------------
|
| 75 |
+
# 1) Main Agent - the personal, trusted, all-purpose assistant
|
| 76 |
+
# (built once, at import time, exactly like the sub agents)
|
| 77 |
# ---------------------------------------------------------------------------
|
| 78 |
+
_supervisor_builder = create_supervisor(
|
| 79 |
+
agents=ALL_SUB_AGENTS,
|
| 80 |
+
model=_llm,
|
| 81 |
+
prompt=SUPERVISOR_PROMPT,
|
| 82 |
+
supervisor_name=AGENT_NAME,
|
| 83 |
+
add_handoff_back_messages=True,
|
| 84 |
+
output_mode="full_history",
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
main_assistant_agent = _supervisor_builder.compile(
|
| 88 |
+
checkpointer=_checkpointer,
|
| 89 |
+
name=AGENT_NAME,
|
| 90 |
+
)
|
| 91 |
|
| 92 |
|
| 93 |
+
# ---------------------------------------------------------------------------
|
| 94 |
+
# MAIN ASSISTANT AGENTING SYSTEM
|
| 95 |
+
# ---------------------------------------------------------------------------
|
| 96 |
+
def main_agent(
|
| 97 |
+
user_command: str,
|
| 98 |
+
user_attachment: str | None = None,
|
| 99 |
+
thread_id: str = "default",
|
| 100 |
+
) -> str:
|
| 101 |
"""
|
| 102 |
+
Single-shot, synchronous entry point - same call shape as the original
|
| 103 |
+
main_agent(user_command, user_attachment) -> str. `thread_id` is optional
|
| 104 |
+
and only used so multiple separate conversations (as shown in the app's
|
| 105 |
+
sidebar) each keep their own persisted history inside the same
|
| 106 |
+
main_assistant_agent graph/state.
|
| 107 |
"""
|
| 108 |
+
text = user_command
|
| 109 |
+
if user_attachment:
|
| 110 |
+
text = f"{text}\n\n[সংযুক্ত ফাইল: {user_attachment}]"
|
| 111 |
+
|
| 112 |
+
config = {"configurable": {"thread_id": thread_id}}
|
| 113 |
+
result = main_assistant_agent.invoke(
|
| 114 |
+
{"messages": [{"role": "user", "content": text}]},
|
| 115 |
+
config=config,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
)
|
| 117 |
+
final_message = result["messages"][-1]
|
| 118 |
+
return getattr(final_message, "content", str(final_message))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
all_sub_agents.py
CHANGED
|
@@ -1,29 +1,35 @@
|
|
| 1 |
# ---------------------------------------------------------------------------
|
| 2 |
-
#
|
| 3 |
# ---------------------------------------------------------------------------
|
| 4 |
-
from
|
| 5 |
-
from
|
| 6 |
-
from
|
| 7 |
-
from
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 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 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# ---------------------------------------------------------------------------
|
| 2 |
+
#IMPORT_ALL_SUB_AGENTS
|
| 3 |
# ---------------------------------------------------------------------------
|
| 4 |
+
from sub_gents.coding.git_hub_agent.git_hub import _git_hub_agent, AGENT_NAME as GITHUB_NAME
|
| 5 |
+
from sub_gents.coding.git_lab_agent.git_lab import _git_lab_agent, AGENT_NAME as GITLAB_NAME
|
| 6 |
+
from sub_gents.social_media.Facebook_agent.facebook import _facebook_agent, AGENT_NAME as FACEBOOK_NAME
|
| 7 |
+
from sub_gents.social_media.youtube_agent.youtube import _youtube_agent, AGENT_NAME as YOUTUBE_NAME
|
| 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 |
+
]
|
| 27 |
|
| 28 |
+
# Agent names, in the same order, used by app.py to label the Manus-style
|
| 29 |
+
# action timeline (agent handoffs / tool calls) in the UI.
|
| 30 |
+
SUB_AGENT_NAMES = [
|
| 31 |
+
GITHUB_NAME,
|
| 32 |
+
GITLAB_NAME,
|
| 33 |
+
FACEBOOK_NAME,
|
| 34 |
+
YOUTUBE_NAME,
|
| 35 |
+
]
|
app.py
CHANGED
|
@@ -10,10 +10,19 @@ 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
|
| 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
|
|
@@ -27,7 +36,7 @@ os.makedirs(UPLOADS_DIR, exist_ok=True)
|
|
| 27 |
_threads_lock = asyncio.Lock()
|
| 28 |
|
| 29 |
app = FastAPI(title="Personal Assistant")
|
| 30 |
-
app.mount("/
|
| 31 |
|
| 32 |
|
| 33 |
# ---------------------------------------------------------------------------
|
|
@@ -74,26 +83,13 @@ async def _touch_thread(thread_id: str, title: str | None = None):
|
|
| 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("
|
| 97 |
|
| 98 |
|
| 99 |
# ---------------------------------------------------------------------------
|
|
@@ -210,10 +206,9 @@ def _messages_to_turns(messages):
|
|
| 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
|
| 217 |
except Exception:
|
| 218 |
return JSONResponse({"turns": []})
|
| 219 |
|
|
@@ -238,7 +233,7 @@ async def chat(
|
|
| 238 |
text: str = Form(""),
|
| 239 |
attachment_path: str | None = Form(None),
|
| 240 |
):
|
| 241 |
-
graph =
|
| 242 |
config = {"configurable": {"thread_id": thread_id}}
|
| 243 |
|
| 244 |
user_text = text or ""
|
|
|
|
| 10 |
from fastapi.staticfiles import StaticFiles
|
| 11 |
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
|
| 12 |
|
| 13 |
+
from agents import main_assistant_agent, AGENT_NAME
|
| 14 |
from all_sub_agents import SUB_AGENT_NAMES
|
| 15 |
from storage_paths import agent_dir
|
| 16 |
|
| 17 |
+
# ---------------------------------------------------------------------------
|
| 18 |
+
# Resolve paths relative to THIS file, not the process's current working
|
| 19 |
+
# directory (which may differ from the project root depending on how the
|
| 20 |
+
# platform/container launches uvicorn) — avoids "Directory does not exist"
|
| 21 |
+
# errors when mounting /static.
|
| 22 |
+
# ---------------------------------------------------------------------------
|
| 23 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 24 |
+
STATIC_DIR = os.path.join(BASE_DIR, "static")
|
| 25 |
+
|
| 26 |
# ---------------------------------------------------------------------------
|
| 27 |
# PERSISTENCE PATHS — everything (thread index + uploaded files) lives inside
|
| 28 |
# the persistent /agent storage bucket, under this agent's own folder, so a
|
|
|
|
| 36 |
_threads_lock = asyncio.Lock()
|
| 37 |
|
| 38 |
app = FastAPI(title="Personal Assistant")
|
| 39 |
+
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
| 40 |
|
| 41 |
|
| 42 |
# ---------------------------------------------------------------------------
|
|
|
|
| 83 |
_write_threads(threads)
|
| 84 |
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
# ---------------------------------------------------------------------------
|
| 87 |
# API: index page
|
| 88 |
# ---------------------------------------------------------------------------
|
| 89 |
@app.get("/")
|
| 90 |
async def index():
|
| 91 |
from fastapi.responses import FileResponse
|
| 92 |
+
return FileResponse(os.path.join(STATIC_DIR, "index.html"))
|
| 93 |
|
| 94 |
|
| 95 |
# ---------------------------------------------------------------------------
|
|
|
|
| 206 |
# ---------------------------------------------------------------------------
|
| 207 |
@app.get("/api/history")
|
| 208 |
async def history(thread_id: str):
|
|
|
|
| 209 |
config = {"configurable": {"thread_id": thread_id}}
|
| 210 |
try:
|
| 211 |
+
state = await main_assistant_agent.aget_state(config)
|
| 212 |
except Exception:
|
| 213 |
return JSONResponse({"turns": []})
|
| 214 |
|
|
|
|
| 233 |
text: str = Form(""),
|
| 234 |
attachment_path: str | None = Form(None),
|
| 235 |
):
|
| 236 |
+
graph = main_assistant_agent
|
| 237 |
config = {"configurable": {"thread_id": thread_id}}
|
| 238 |
|
| 239 |
user_text = text or ""
|
storage_paths.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
| 1 |
import os
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
# ---------------------------------------------------------------------------
|
| 4 |
# /agent is expected to be a PERSISTENT storage bucket (mounted with write
|
|
@@ -7,6 +10,14 @@ import os
|
|
| 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 |
|
|
@@ -16,16 +27,29 @@ def agent_dir(agent_name: str) -> str:
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
probe = os.path.join(path, "
|
| 24 |
try:
|
| 25 |
with open(probe, "w") as f:
|
| 26 |
f.write("ok")
|
| 27 |
-
|
| 28 |
-
except OSError as exc:
|
| 29 |
raise RuntimeError(
|
| 30 |
f"'{path}' পাথে write access পাওয়া যায়নি। এই অ্যাপের সব agent-এর "
|
| 31 |
f"memory/state '/agent' নামের একটি persistent storage বাকেটে রাখা হয় — "
|
|
@@ -34,4 +58,32 @@ def agent_dir(agent_name: str) -> str:
|
|
| 34 |
f"(আসল এরর: {exc})"
|
| 35 |
) from exc
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
return path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
+
import sqlite3
|
| 3 |
+
|
| 4 |
+
from langgraph.checkpoint.sqlite import SqliteSaver
|
| 5 |
|
| 6 |
# ---------------------------------------------------------------------------
|
| 7 |
# /agent is expected to be a PERSISTENT storage bucket (mounted with write
|
|
|
|
| 10 |
# (sqlite state, thread index, uploaded files) is written ONLY under here,
|
| 11 |
# so it survives Space restarts/redeploys instead of living on the
|
| 12 |
# ephemeral container filesystem.
|
| 13 |
+
#
|
| 14 |
+
# NOTE: many "persistent storage" buckets (including S3-backed / object
|
| 15 |
+
# storage mounts) behave like S3, not like a normal local disk: no real
|
| 16 |
+
# file locking, no shared-memory mmap, sometimes only whole-object
|
| 17 |
+
# read/write. SQLite's default WAL journal mode depends on mmap + proper
|
| 18 |
+
# file locks and can hang or quietly corrupt on that kind of storage — so
|
| 19 |
+
# every checkpointer created here is forced into the plain rollback
|
| 20 |
+
# journal instead (see open_agent_sqlite below).
|
| 21 |
# ---------------------------------------------------------------------------
|
| 22 |
AGENT_ROOT = "/agent"
|
| 23 |
|
|
|
|
| 27 |
Returns (and creates if needed) /agent/<agent_name>/, and fails loudly
|
| 28 |
with a clear message if that location isn't actually writable — instead
|
| 29 |
of silently falling back to ephemeral local storage.
|
| 30 |
+
|
| 31 |
+
The write-check itself is written to be tolerant of S3-style / object
|
| 32 |
+
storage buckets: it only requires that a file can be CREATED there.
|
| 33 |
+
Deleting it afterwards is best-effort only, since some object-storage
|
| 34 |
+
gateways don't support an immediate delete-after-create and that alone
|
| 35 |
+
doesn't mean the bucket isn't writable — treating it as fatal caused a
|
| 36 |
+
false-positive crash on exactly that kind of storage.
|
| 37 |
"""
|
| 38 |
path = os.path.join(AGENT_ROOT, agent_name)
|
| 39 |
+
try:
|
| 40 |
+
os.makedirs(path, exist_ok=True)
|
| 41 |
+
except Exception as exc:
|
| 42 |
+
raise RuntimeError(
|
| 43 |
+
f"'{path}' ফোল্ডার তৈরি করা যায়নি। '/agent' একটি persistent storage "
|
| 44 |
+
f"বাকেট (যেমন HF Space-এর Persistent Storage) হিসেবে সঠিক পাথে "
|
| 45 |
+
f"মাউন্ট করা আছে কিনা যাচাই করুন. (আসল এরর: {exc})"
|
| 46 |
+
) from exc
|
| 47 |
|
| 48 |
+
probe = os.path.join(path, "write_test.tmp")
|
| 49 |
try:
|
| 50 |
with open(probe, "w") as f:
|
| 51 |
f.write("ok")
|
| 52 |
+
except Exception as exc:
|
|
|
|
| 53 |
raise RuntimeError(
|
| 54 |
f"'{path}' পাথে write access পাওয়া যায়নি। এই অ্যাপের সব agent-এর "
|
| 55 |
f"memory/state '/agent' নামের একটি persistent storage বাকেটে রাখা হয় — "
|
|
|
|
| 58 |
f"(আসল এরর: {exc})"
|
| 59 |
) from exc
|
| 60 |
|
| 61 |
+
try:
|
| 62 |
+
os.remove(probe)
|
| 63 |
+
except Exception:
|
| 64 |
+
pass # best-effort cleanup only, see docstring above
|
| 65 |
+
|
| 66 |
return path
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def open_agent_sqlite(db_path: str) -> SqliteSaver:
|
| 70 |
+
"""
|
| 71 |
+
Opens (creating if needed) a sqlite-backed LangGraph checkpointer at
|
| 72 |
+
db_path, configured to be safe on S3-style / network object storage:
|
| 73 |
+
plain rollback journal instead of WAL (no mmap / shared-memory
|
| 74 |
+
dependency) and full fsync-on-commit durability.
|
| 75 |
+
"""
|
| 76 |
+
try:
|
| 77 |
+
conn = sqlite3.connect(db_path, check_same_thread=False)
|
| 78 |
+
conn.execute("PRAGMA journal_mode=DELETE;")
|
| 79 |
+
conn.execute("PRAGMA synchronous=FULL;")
|
| 80 |
+
saver = SqliteSaver(conn)
|
| 81 |
+
saver.setup()
|
| 82 |
+
return saver
|
| 83 |
+
except Exception as exc:
|
| 84 |
+
raise RuntimeError(
|
| 85 |
+
f"'{db_path}'-এ sqlite মেমরি ফাইল খোলা/সেটআপ করা যায়নি। যদি '/agent' "
|
| 86 |
+
f"একটি S3-স্টাইল object storage বাকেট হয়, নিশ্চিত করুন সেটি সাধারণ "
|
| 87 |
+
f"ফাইল read/write/delete সাপোর্ট করে (শুধু whole-object PUT নয়)। "
|
| 88 |
+
f"(আসল এরর: {exc})"
|
| 89 |
+
) from exc
|