Spaces:
Runtime error
Runtime error
Upload 6 files
Browse files- Dockerfile +12 -9
- agents.py +75 -98
- all_sub_agents.py +24 -21
- app.py +290 -19
- requirements.txt +26 -8
- 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
|
| 47 |
EXPOSE 7860
|
| 48 |
|
| 49 |
# ── Healthcheck ────────────────────────────────────────────────────────────────
|
| 50 |
HEALTHCHECK --interval=60s --timeout=10s --start-period=30s \
|
| 51 |
-
CMD curl -f http://localhost:7860/
|
| 52 |
|
| 53 |
-
# ── Launch
|
| 54 |
-
CMD ["
|
|
|
|
| 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
|
| 4 |
-
from
|
| 5 |
-
from
|
| 6 |
-
|
|
|
|
|
|
|
| 7 |
|
| 8 |
load_dotenv()
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
# ---------------------------------------------------------------------------# MAIN AGENT LLM
|
| 12 |
# ---------------------------------------------------------------------------
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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,
|
| 49 |
-
"
|
|
|
|
|
|
|
| 50 |
"মোঃ ছামিউল ইসলাম (MD SAMIUL ISLAM) এর পার্সোনাল এজেন্ট এবং এসিস্ট্যান্ট "
|
| 51 |
"ছামিউল আমাকে তৈরি করেছে"
|
| 52 |
)
|
| 53 |
|
| 54 |
-
|
| 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
|
| 63 |
-
"When delegating tasks that require real GitHub
|
| 64 |
-
"
|
| 65 |
-
"
|
| 66 |
-
"
|
| 67 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
"আমি ছামিউল এর তৈরি একটা ভার্চুয়াল রোবট বা এআই এসিস্ট্যান্ট এজেন্ট"
|
| 69 |
)
|
| 70 |
|
|
|
|
| 71 |
|
| 72 |
# ---------------------------------------------------------------------------
|
| 73 |
-
#
|
| 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 |
-
|
| 96 |
-
|
| 97 |
-
|
| 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 |
-
|
| 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 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 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 |
-
#
|
| 3 |
# ---------------------------------------------------------------------------
|
| 4 |
-
from
|
| 5 |
-
from
|
| 6 |
-
from
|
| 7 |
-
from
|
| 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 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
from
|
|
|
|
|
|
|
|
|
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
def chat_fn(message, history):
|
| 7 |
-
"""
|
| 8 |
-
Gradio multimodal ChatInterface callback.
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
"""
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
mcp
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|