File size: 22,773 Bytes
d712cef
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
#!/usr/bin/python3
import os
import sys
import subprocess
from typing import Annotated, Sequence, TypedDict, Literal
from pydantic import BaseModel, Field

try:
    from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
    from langchain_core.tools import tool
    from langgraph.graph import StateGraph, START, END
    from langgraph.graph.message import add_messages
    from langgraph.prebuilt import ToolNode
    from langgraph.checkpoint.memory import MemorySaver
    from langchain_google_genai import ChatGoogleGenerativeAI
except ImportError as e:
    print(f"Missing required library: {e}")
    sys.exit(1)

if "GOOGLE_API_KEY" not in os.environ:
    os.environ["GOOGLE_API_KEY"] = "AIzaSyBNpkJkdsEHFDezctWxPKhAuFrIfFcNy1s"

PYTHON_BIN = "/Volumes/ssd2/TEXBASE/venv/bin/python3"
SRC_DIR = "/Volumes/ssd2/TEXBASE/src"

# ══════════════════════════════════════════════════════════════════════════════
#  GRAPH STATE & REAL TOOLS
# ══════════════════════════════════════════════════════════════════════════════
class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], add_messages]
    next_node: str

# --- CashFlow Tool ---
class CashFlowArgs(BaseModel):
    input_type: Literal["text", "file", "email"] = Field(
        description="The format of the input. Use 'text' for plain text descriptions, 'file' for PDF/image paths, and 'email' for email text file paths."
    )
    input_value: str = Field(
        description="The actual text description, or the absolute file path."
    )

@tool(args_schema=CashFlowArgs)
def cashflow_tool(input_type: str, input_value: str) -> str:
    """Logs financial transactions using the CashFlowCareTaker agent based on text, a file, or an email."""
    cashflow_dir = os.path.join(SRC_DIR, "CashFlowCareTaker")
    script_path = os.path.join(cashflow_dir, "main.py")
    
    cmd = [PYTHON_BIN, script_path]
    if input_type == "text":
        cmd.extend(["--text", input_value])
    elif input_type == "file":
        cmd.extend(["--file", input_value])
    elif input_type == "email":
        cmd.extend(["--email", input_value])
        
    print(f"\n[Tool Execution] Running CashFlow agent with cmd: {' '.join(cmd)}")
    try:
        res = subprocess.run(cmd, cwd=cashflow_dir, capture_output=True, text=True)
        return f"CashFlow Output:\n{res.stdout}\nErrors if any:\n{res.stderr}"
    except Exception as e:
        return f"Failed to execute CashFlow Agent: {e}"

# --- PO & Quotation Tools ---
class ProcessPOArgs(BaseModel):
    file_path: str = Field(description="Absolute path to the PO file (PDF or image) to process/extract data from. Use this if the user wants to 'enter', 'read', or process a new document so it goes into the database.")

@tool(args_schema=ProcessPOArgs)
def process_po_tool(file_path: str) -> str:
    """Processes a Purchase Order file using the PO pipeline (extracts to database). If the user asks to get quotations for a raw file/image, YOU MUST RUN THIS TOOL FIRST to extract its data before predicting quotations!"""
    po_dir = os.path.join(SRC_DIR, "PO:Quotation")
    script_path = os.path.join(po_dir, "po_processor.py")
    
    cmd = [PYTHON_BIN, script_path, "--file", file_path]
    print(f"\n[Tool Execution] Running PO_Manager (Processor) on {file_path}...")
    try:
        res = subprocess.run(cmd, cwd=po_dir, capture_output=True, text=True)
        return f"PO Processing Output:\n{res.stdout}\nErrors if any:\n{res.stderr}"
    except Exception as e:
        return f"Failed to execute PO Processor: {e}"


class PredictQuotationArgs(BaseModel):
    source_file: str = Field(default="", description="The exact filename (e.g. 'my_po.pdf' or 'image.png') ALREADY present in the PO database. If the file was never processed, run process_po_tool first!")
    table_name: str = Field(default="", description="Optional: specific table name to query. If source_file is omitted, you MUST provide this.")

@tool(args_schema=PredictQuotationArgs)
def predict_quotation_tool(source_file: str = "", table_name: str = "") -> str:
    """Predicts pricing for PO line items using the Quotation Predictor tool and Google Search. Document must already be in the PO database."""
    po_dir = os.path.join(SRC_DIR, "PO:Quotation")
    script_path = os.path.join(po_dir, "quotation_predictor.py")
    
    cmd = [PYTHON_BIN, script_path]
    if source_file:
        cmd.extend(["--source", source_file])
    if table_name:
        cmd.extend(["--table", table_name])
        
    print(f"\n[Tool Execution] Running PO_Manager (Predictor) for source_file='{source_file}' table='{table_name}'...")
    try:
        res = subprocess.run(cmd, cwd=po_dir, capture_output=True, text=True)
        return f"Quotation Prediction Output:\n{res.stdout}\nErrors if any:\n{res.stderr}"
    except Exception as e:
        return f"Failed to execute Quotation Predictor: {e}"


# --- Research Tools ---
class RunDeepResearchArgs(BaseModel):
    query: str = Field(default="", description="Optional: specific research prompt to investigate. Leaves default if blank.")
    topic: str = Field(default="Deep_Research_Textile_Report", description="Optional: Short topic name for file and table generation.")

@tool(args_schema=RunDeepResearchArgs)
def run_deep_research_tool(query: str = "", topic: str = "Deep_Research_Textile_Report") -> str:
    """Runs the Deep Research Agent to generate a PDF report. If a query is provided, it researches that specific query and saves it under the topic name. If not, it runs the default global textile prompts. Very slow, takes several minutes."""
    research_dir = os.path.join(SRC_DIR, "ResearchAgent")
    brain_script = os.path.join(research_dir, "researchBrain.py")
    parser_script = os.path.join(research_dir, "Information_parser.py")
    
    # 1. Generate the PDF Report
    cmd_brain = [PYTHON_BIN, brain_script]
    if query:
        cmd_brain.extend(["--query", query, "--topic", topic])
    
    print(f"\n[Tool Execution] Running Research_Manager (Deep Research PDF generation)...")
    try:
        res_brain = subprocess.run(cmd_brain, cwd=research_dir, capture_output=True, text=True)
        if res_brain.returncode != 0:
             return f"Failed during Deep Research Phase:\n{res_brain.stderr}"
    except Exception as e:
        return f"Failed to execute Deep Research: {e}"

    # 2. Extract Data into Database
    safe_topic = re.sub(r'[^A-Za-z0-9_\-]', '_', topic)
    pdf_filename = f"{safe_topic}.pdf"
    
    # Generate daily table name (e.g. 2026_03_10_Topic)
    date_str = datetime.datetime.now().strftime("%Y_%m_%d")
    table_name = f"{date_str}_{safe_topic}"

    cmd_parser = [PYTHON_BIN, parser_script, "--target", pdf_filename, "--table", table_name]
    print(f"\n[Tool Execution] Running Research_Manager (Information Parsing into {table_name})...")
    try:
        res_parser = subprocess.run(cmd_parser, cwd=research_dir, capture_output=True, text=True)
        if res_parser.returncode != 0:
             return f"Failed during Information Parsing Phase:\n{res_parser.stderr}"
    except Exception as e:
        return f"Failed to execute Information Parser: {e}"
        
    return f"Research Complete! Database updated and you can view research on the trends page. Table created: {table_name}"


class ParseResearchInfoArgs(BaseModel):
    pass

@tool(args_schema=ParseResearchInfoArgs)
def parse_research_information_tool() -> str:
    """Parses all PDFs in the research_pdf folder to extract self-contained news snippets into the default news_items table."""
    research_dir = os.path.join(SRC_DIR, "ResearchAgent")
    script_path = os.path.join(research_dir, "Information_parser.py")
    
    cmd = [PYTHON_BIN, script_path]
    print(f"\n[Tool Execution] Running Research_Manager (Information Parser Bulk)...")
    try:
        res = subprocess.run(cmd, cwd=research_dir, capture_output=True, text=True)
        return f"Information Parser Output:\n{res.stdout}\nErrors if any:\n{res.stderr}"
    except Exception as e:
        return f"Failed to execute Information Parser: {e}"


class GenerateColdEmailArgs(BaseModel):
    # No arguments needed, it draws from the importyeti database queue
    pass

@tool(args_schema=GenerateColdEmailArgs)
def generate_cold_email_tool() -> str:
    """Generates a cold email outreach draft for the next queued company by executing a LangGraph pipeline that verifies emails with Hunter and enriches with Deep Research."""
    email_dir = os.path.join(SRC_DIR, "ColdEmail")
    script_path = os.path.join(email_dir, "email_specific.py")
    
    cmd = [PYTHON_BIN, script_path]
    print(f"\n[Tool Execution] Running Email_Manager (Cold Email Generation)...")
    try:
        res = subprocess.run(cmd, cwd=email_dir, capture_output=True, text=True)
        return f"Cold Email Output:\n{res.stdout}\nErrors if any:\n{res.stderr}"
    except Exception as e:
        return f"Failed to execute Cold Email Generator: {e}"


class ProcessNextFollowUpArgs(BaseModel):
    pass

@tool(args_schema=ProcessNextFollowUpArgs)
def process_next_followup_email_tool() -> str:
    """Processes the next NEW (unprocessed) email in the central FollowUp inbox, routing it and generating a draft reply."""
    followup_dir = os.path.join(SRC_DIR, "FollowUp")
    cmd = [PYTHON_BIN, os.path.join(followup_dir, "main.py"), "--run"]
    print(f"\n[Tool Execution] Running FollowUp_Manager (Process Next Email)...")
    try:
        res = subprocess.run(cmd, cwd=followup_dir, capture_output=True, text=True)
        return f"FollowUp Output:\n{res.stdout}\nErrors if any:\n{res.stderr}"
    except Exception as e:
        return f"Failed to execute FollowUp Agent: {e}"


class SimulateFollowUpArgs(BaseModel):
    email_text: str = Field(description="The body text of the email")
    sender: str = Field(default="unknown@email.com", description="The sender's email address")
    subject: str = Field(default="No Subject", description="The subject line of the email")

@tool(args_schema=SimulateFollowUpArgs)
def simulate_followup_email_tool(email_text: str, sender: str = "unknown@email.com", subject: str = "No Subject") -> str:
    """Simulates receiving an incoming email without checking the real inbox, runs the routing, and outputs the draft."""
    followup_dir = os.path.join(SRC_DIR, "FollowUp")
    cmd = [PYTHON_BIN, os.path.join(followup_dir, "main.py"), "--simulate", email_text, "--from", sender, "--subject", subject]
    print(f"\n[Tool Execution] Running FollowUp_Manager (Simulate Email)...")
    try:
        res = subprocess.run(cmd, cwd=followup_dir, capture_output=True, text=True)
        return f"FollowUp Output:\n{res.stdout}\nErrors if any:\n{res.stderr}"
    except Exception as e:
        return f"Failed to execute FollowUp Agent: {e}"


class ListInboxArgs(BaseModel):
    pass

@tool(args_schema=ListInboxArgs)
def list_inbox_emails_tool() -> str:
    """Lists all emails currently in the central FollowUp inbox, highlighting which ones are [NEW] vs [UNDER REVIEW]."""
    followup_dir = os.path.join(SRC_DIR, "FollowUp")
    cmd = [PYTHON_BIN, os.path.join(followup_dir, "main.py"), "--list"]
    print(f"\n[Tool Execution] Running FollowUp_Manager (List Inbox)...")
    try:
        res = subprocess.run(cmd, cwd=followup_dir, capture_output=True, text=True)
        return f"FollowUp Output:\n{res.stdout}\nErrors if any:\n{res.stderr}"
    except Exception as e:
        return f"Failed to execute FollowUp Agent: {e}"


class ShowDraftArgs(BaseModel):
    inbox_id: int = Field(description="The integer inbox ID to view the drafted reply for.")

@tool(args_schema=ShowDraftArgs)
def show_draft_reply_tool(inbox_id: int) -> str:
    """Shows the original email and the drafted reply for a specific inbox ID."""
    followup_dir = os.path.join(SRC_DIR, "FollowUp")
    cmd = [PYTHON_BIN, os.path.join(followup_dir, "main.py"), "--draft", str(inbox_id)]
    print(f"\n[Tool Execution] Running FollowUp_Manager (Show Draft #{inbox_id})...")
    try:
        res = subprocess.run(cmd, cwd=followup_dir, capture_output=True, text=True)
        return f"FollowUp Output:\n{res.stdout}\nErrors if any:\n{res.stderr}"
    except Exception as e:
        return f"Failed to execute FollowUp Agent: {e}"


class IngestEmailFileArgs(BaseModel):
    file_path: str = Field(description="Absolute path to the email .txt file")
    sender: str = Field(default="unknown@email.com", description="The sender's email address")
    subject: str = Field(default="No Subject", description="The subject line of the email")

@tool(args_schema=IngestEmailFileArgs)
def ingest_email_file_tool(file_path: str, sender: str = "unknown@email.com", subject: str = "No Subject") -> str:
    """Ingests a text file as an email, saves it to the central inbox, and processes it."""
    followup_dir = os.path.join(SRC_DIR, "FollowUp")
    cmd = [PYTHON_BIN, os.path.join(followup_dir, "main.py"), "--email", file_path, "--from", sender, "--subject", subject]
    print(f"\n[Tool Execution] Running FollowUp_Manager (Ingest Email File)...")
    try:
        res = subprocess.run(cmd, cwd=followup_dir, capture_output=True, text=True)
        return f"FollowUp Output:\n{res.stdout}\nErrors if any:\n{res.stderr}"
    except Exception as e:
        return f"Failed to execute FollowUp Agent: {e}"


finance_tools = [cashflow_tool]
po_tools = [process_po_tool, predict_quotation_tool]
research_tools = [run_deep_research_tool, parse_research_information_tool]
email_tools = [generate_cold_email_tool]
followup_tools = [process_next_followup_email_tool, simulate_followup_email_tool, list_inbox_emails_tool, show_draft_reply_tool, ingest_email_file_tool]

# ══════════════════════════════════════════════════════════════════════════════
#  MULTI-AGENT PERSONAS & ROUTING 
# ══════════════════════════════════════════════════════════════════════════════
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
finance_agent_llm = llm.bind_tools(finance_tools)
po_agent_llm = llm.bind_tools(po_tools)
research_agent_llm = llm.bind_tools(research_tools)
email_agent_llm = llm.bind_tools(email_tools)
followup_agent_llm = llm.bind_tools(followup_tools)

class RouteDecision(BaseModel):
    next_node: Literal["Finance_Manager", "PO_Manager", "Research_Manager", "Email_Manager", "FollowUp_Manager", "FINISH"] = Field(
        description="Select the specialized agent or FINISH"
    )

supervisor_router = llm.with_structured_output(RouteDecision)

def supervisor_node(state: AgentState):
    sys_prompt = (
        "You are the Head Superagent orchestrator overseeing the AI system.\n"
        " - Finance_Manager: For parsing and recording cash flow, income, expense, or financial transactions from text, files, or emails.\n"
        " - PO_Manager: For handling processing of Purchase Orders (PO) or making quotation pricing predictions on PO line items.\n"
        " - Research_Manager: For conducting deep textile market research to generate PDF reports, or parsing those PDFs into a database of news snippets.\n"
        " - Email_Manager: For generating outreach and drafting cold emails for new prospects or companies.\n"
        " - FollowUp_Manager: For managing the central email inbox, routing incoming emails to the correct databases, evaluating threads, simulating inbound emails, and drafting replies (under review).\n"
        " - FINISH: If the user request is fully satisfied or no tools need calling.\n"
        "Analyze the user's latest request and route it. We will add more agents later."
    )
    decision = supervisor_router.invoke([("system", sys_prompt)] + state["messages"])
    return {"next_node": decision.next_node}

def finance_manager_node(state: AgentState):
    sys_prompt = "You are the Finance_Manager. You use `cashflow_tool`. Acknowledge execution results."
    res = finance_agent_llm.invoke([("system", sys_prompt)] + state["messages"])
    return {"messages": [res]}

def po_manager_node(state: AgentState):
    sys_prompt = "You are the PO_Manager. You use `process_po_tool` and `predict_quotation_tool`. Acknowledge execution results."
    res = po_agent_llm.invoke([("system", sys_prompt)] + state["messages"])
    return {"messages": [res]}

def research_manager_node(state: AgentState):
    sys_prompt = "You are the Research_Manager. You use `run_deep_research_tool` and `parse_research_information_tool`. Acknowledge execution results."
    res = research_agent_llm.invoke([("system", sys_prompt)] + state["messages"])
    return {"messages": [res]}

def email_manager_node(state: AgentState):
    sys_prompt = "You are the Email_Manager. You use `generate_cold_email_tool`. Acknowledge execution results."
    res = email_agent_llm.invoke([("system", sys_prompt)] + state["messages"])
    return {"messages": [res]}

def followup_manager_node(state: AgentState):
    sys_prompt = "You are the FollowUp_Manager. You use `process_next_followup_email_tool`, `simulate_followup_email_tool`, `list_inbox_emails_tool`, `show_draft_reply_tool`, and `ingest_email_file_tool`. Acknowledge execution results."
    res = followup_agent_llm.invoke([("system", sys_prompt)] + state["messages"])
    return {"messages": [res]}

# ---- Inter-Agent Routing ----
def route_supervisor(state: AgentState):
    if state.get("next_node") == "FINISH":
        return END
    return state.get("next_node", "FINISH")

def route_finance_manager(state: AgentState):
    last_msg = state["messages"][-1]
    return "finance_tool_node" if (hasattr(last_msg, "tool_calls") and last_msg.tool_calls) else "supervisor"

def route_po_manager(state: AgentState):
    last_msg = state["messages"][-1]
    return "po_tool_node" if (hasattr(last_msg, "tool_calls") and last_msg.tool_calls) else "supervisor"

def route_research_manager(state: AgentState):
    last_msg = state["messages"][-1]
    return "research_tool_node" if (hasattr(last_msg, "tool_calls") and last_msg.tool_calls) else "supervisor"

def route_email_manager(state: AgentState):
    last_msg = state["messages"][-1]
    return "email_tool_node" if (hasattr(last_msg, "tool_calls") and last_msg.tool_calls) else "supervisor"

def route_followup_manager(state: AgentState):
    last_msg = state["messages"][-1]
    return "followup_tool_node" if (hasattr(last_msg, "tool_calls") and last_msg.tool_calls) else "supervisor"

# ══════════════════════════════════════════════════════════════════════════════
#  BUILD GRAPH
# ══════════════════════════════════════════════════════════════════════════════
builder = StateGraph(AgentState)

builder.add_node("supervisor", supervisor_node)
builder.add_node("Finance_Manager", finance_manager_node)
builder.add_node("PO_Manager", po_manager_node)
builder.add_node("Research_Manager", research_manager_node)
builder.add_node("Email_Manager", email_manager_node)
builder.add_node("FollowUp_Manager", followup_manager_node)

builder.add_node("finance_tool_node", ToolNode(finance_tools))
builder.add_node("po_tool_node", ToolNode(po_tools))
builder.add_node("research_tool_node", ToolNode(research_tools))
builder.add_node("email_tool_node", ToolNode(email_tools))
builder.add_node("followup_tool_node", ToolNode(followup_tools))

builder.add_edge(START, "supervisor")
builder.add_conditional_edges("supervisor", route_supervisor)

builder.add_conditional_edges("Finance_Manager", route_finance_manager)
builder.add_edge("finance_tool_node", "Finance_Manager")

builder.add_conditional_edges("PO_Manager", route_po_manager)
builder.add_edge("po_tool_node", "PO_Manager")

builder.add_conditional_edges("Research_Manager", route_research_manager)
builder.add_edge("research_tool_node", "Research_Manager")

builder.add_conditional_edges("Email_Manager", route_email_manager)
builder.add_edge("email_tool_node", "Email_Manager")

builder.add_conditional_edges("FollowUp_Manager", route_followup_manager)
builder.add_edge("followup_tool_node", "FollowUp_Manager")

memory = MemorySaver()
graph = builder.compile(checkpointer=memory)

# ══════════════════════════════════════════════════════════════════════════════
#  CLI
# ══════════════════════════════════════════════════════════════════════════════
def start_cli():
    print("="*60)
    print(" πŸš€ HEAD SUPERAGENT INITIALIZED (LangGraph) ")
    print("    Currently connected agents: Finance_Manager, PO_Manager, Research_Manager, Email_Manager, FollowUp_Manager")
    print("    (Room reserved for future agents...)")
    print("="*60)
    
    thread_id = input("Enter your Session Thread ID (e.g., 'session_main'): ").strip()
    if not thread_id: thread_id = "default_thread"
    config = {"configurable": {"thread_id": thread_id}}
    
    print(f"\n[System] Restoring persistent checks for thread: {thread_id}")
    
    while True:
        try:
            user_in = input("\nUser> ")
            if user_in.lower() in ["quit", "exit"]:
                print("Exiting...")
                break
                
            printed_ids = set()
            for ev in graph.stream({"messages": [HumanMessage(content=user_in)]}, config, stream_mode="values"):
                msg = ev["messages"][-1]
                if isinstance(msg, AIMessage) and msg.content and getattr(msg, 'id', None) not in printed_ids:
                    print(f"\n[Agent]: {msg.content}")
                    if hasattr(msg, 'id'): printed_ids.add(msg.id)

        except Exception as e:
            print(f"Error during runtime loop: {e}")
            import traceback; traceback.print_exc()

if __name__ == "__main__":
    start_cli()