samiulttr commited on
Commit
24cd56e
·
verified ·
1 Parent(s): 305be22

Upload 6 files

Browse files
Files changed (5) hide show
  1. .env.example +38 -0
  2. agents.py +50 -45
  3. all_sub_agents.py +29 -23
  4. app.py +14 -19
  5. 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 build_all_sub_agents
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
- 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, "
@@ -42,7 +48,7 @@ GOAL = (
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, "
@@ -63,51 +69,50 @@ BACKSTORY = (
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 = []
 
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
- # IMPORT_ALL_SUB_AGENT_BUILDERS
3
  # ---------------------------------------------------------------------------
4
- from sub_agents.coding.github_agent.github import build_git_hub_agent, AGENT_NAME as GITHUB_NAME
5
- from sub_agents.coding.gitlab_agent.gitlab import build_git_lab_agent, AGENT_NAME as GITLAB_NAME
6
- from sub_agents.social_media.facebook_agent.facebook import build_facebook_agent, AGENT_NAME as FACEBOOK_NAME
7
- from sub_agents.social_media.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
 
 
 
 
 
 
 
 
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 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
@@ -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("/agent/static", StaticFiles(directory="static"), name="static")
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("static/index.html")
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 graph.aget_state(config)
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 = await get_main_agent()
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
- 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 বাকেটে রাখা হয় — "
@@ -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