gcharanteja commited on
Commit Β·
7cbaace
1
Parent(s): 2881c4f
Refactor Telegram agent initialization and shared state management; add error handling and model path resolution
Browse files- .dockerignore +10 -0
- main.py +23 -15
- shared_state.py +3 -0
- telegram_agent.py +66 -22
.dockerignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
*.env
|
| 3 |
+
.venv/
|
| 4 |
+
__pycache__/
|
| 5 |
+
*.pyc
|
| 6 |
+
models/
|
| 7 |
+
*.gguf
|
| 8 |
+
.qwen
|
| 9 |
+
.git/
|
| 10 |
+
.gitignore
|
main.py
CHANGED
|
@@ -95,31 +95,35 @@ def start_jira_server():
|
|
| 95 |
def start_telegram_and_mcp():
|
| 96 |
"""Run the Telegram agent + MCP server in a dedicated event loop."""
|
| 97 |
import shared_state
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
_init_embedder, _init_pinecone, bot, llm,
|
| 101 |
-
)
|
| 102 |
|
| 103 |
# Initialize components to detect LLM
|
| 104 |
try:
|
| 105 |
-
_init_embedder()
|
| 106 |
-
_init_llm()
|
| 107 |
-
_init_pinecone()
|
| 108 |
-
_init_telegram_bot()
|
| 109 |
except Exception as e:
|
| 110 |
state.error = f"Failed to init components: {str(e)}"
|
|
|
|
|
|
|
| 111 |
print(f"[orchestrator] ERROR: {state.error}")
|
| 112 |
return
|
| 113 |
|
| 114 |
-
# Set refs
|
| 115 |
-
shared_state.llm_instance = llm
|
| 116 |
-
shared_state.telegram_bot = bot
|
|
|
|
|
|
|
| 117 |
|
| 118 |
# Run the MCP + Telegram loop
|
| 119 |
try:
|
| 120 |
-
asyncio.run(start_telegram_with_mcp())
|
| 121 |
except Exception as e:
|
| 122 |
state.error = f"Telegram/MCP failed: {str(e)}"
|
|
|
|
|
|
|
| 123 |
print(f"[orchestrator] ERROR: {state.error}")
|
| 124 |
|
| 125 |
|
|
@@ -185,7 +189,7 @@ def status_mcp():
|
|
| 185 |
|
| 186 |
if session is None or tools is None:
|
| 187 |
return {"service": "mcp_server", "status": "down", "tools_count": 0,
|
| 188 |
-
"tools": [], "error": "MCP server not initialized"}
|
| 189 |
return {"service": "mcp_server", "status": "live", "tools_count": len(tools),
|
| 190 |
"tools": [t["function"]["name"] for t in tools]}
|
| 191 |
|
|
@@ -200,11 +204,15 @@ def status_telegram():
|
|
| 200 |
bot_status = "live" if b is not None else "down"
|
| 201 |
llm_status = "loaded" if l is not None else "not_loaded"
|
| 202 |
|
| 203 |
-
model_path = os.environ.get(
|
|
|
|
|
|
|
| 204 |
model_exists = os.path.exists(model_path)
|
| 205 |
|
| 206 |
return {"service": "telegram_bot", "bot_status": bot_status, "llm_status": llm_status,
|
| 207 |
-
"model_path": model_path, "model_downloaded": model_exists
|
|
|
|
|
|
|
| 208 |
|
| 209 |
|
| 210 |
@app.get("/status")
|
|
|
|
| 95 |
def start_telegram_and_mcp():
|
| 96 |
"""Run the Telegram agent + MCP server in a dedicated event loop."""
|
| 97 |
import shared_state
|
| 98 |
+
import telegram_agent as tg
|
| 99 |
+
shared_state.startup_stage = "initializing"
|
|
|
|
|
|
|
| 100 |
|
| 101 |
# Initialize components to detect LLM
|
| 102 |
try:
|
| 103 |
+
tg._init_embedder()
|
| 104 |
+
tg._init_llm()
|
| 105 |
+
tg._init_pinecone()
|
| 106 |
+
tg._init_telegram_bot()
|
| 107 |
except Exception as e:
|
| 108 |
state.error = f"Failed to init components: {str(e)}"
|
| 109 |
+
shared_state.telegram_error = state.error
|
| 110 |
+
shared_state.startup_stage = "failed"
|
| 111 |
print(f"[orchestrator] ERROR: {state.error}")
|
| 112 |
return
|
| 113 |
|
| 114 |
+
# Set refs from live module globals (not from stale imported names)
|
| 115 |
+
shared_state.llm_instance = tg.llm
|
| 116 |
+
shared_state.telegram_bot = tg.bot
|
| 117 |
+
shared_state.model_path = tg.resolved_model_path or tg.MODEL_PATH
|
| 118 |
+
shared_state.telegram_error = None
|
| 119 |
|
| 120 |
# Run the MCP + Telegram loop
|
| 121 |
try:
|
| 122 |
+
asyncio.run(tg.start_telegram_with_mcp())
|
| 123 |
except Exception as e:
|
| 124 |
state.error = f"Telegram/MCP failed: {str(e)}"
|
| 125 |
+
shared_state.telegram_error = state.error
|
| 126 |
+
shared_state.startup_stage = "failed"
|
| 127 |
print(f"[orchestrator] ERROR: {state.error}")
|
| 128 |
|
| 129 |
|
|
|
|
| 189 |
|
| 190 |
if session is None or tools is None:
|
| 191 |
return {"service": "mcp_server", "status": "down", "tools_count": 0,
|
| 192 |
+
"tools": [], "error": shared_state.telegram_error or "MCP server not initialized"}
|
| 193 |
return {"service": "mcp_server", "status": "live", "tools_count": len(tools),
|
| 194 |
"tools": [t["function"]["name"] for t in tools]}
|
| 195 |
|
|
|
|
| 204 |
bot_status = "live" if b is not None else "down"
|
| 205 |
llm_status = "loaded" if l is not None else "not_loaded"
|
| 206 |
|
| 207 |
+
model_path = shared_state.model_path or os.environ.get(
|
| 208 |
+
"MODEL_PATH", os.path.join(BASE_DIR, "models", "microsoft_Phi-4-mini-instruct-Q4_K_M.gguf")
|
| 209 |
+
)
|
| 210 |
model_exists = os.path.exists(model_path)
|
| 211 |
|
| 212 |
return {"service": "telegram_bot", "bot_status": bot_status, "llm_status": llm_status,
|
| 213 |
+
"model_path": model_path, "model_downloaded": model_exists,
|
| 214 |
+
"startup_stage": shared_state.startup_stage,
|
| 215 |
+
"error": shared_state.telegram_error}
|
| 216 |
|
| 217 |
|
| 218 |
@app.get("/status")
|
shared_state.py
CHANGED
|
@@ -6,3 +6,6 @@ mcp_session = None
|
|
| 6 |
openai_tools = None
|
| 7 |
telegram_bot = None
|
| 8 |
llm_instance = None
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
openai_tools = None
|
| 7 |
telegram_bot = None
|
| 8 |
llm_instance = None
|
| 9 |
+
model_path = None
|
| 10 |
+
telegram_error = None
|
| 11 |
+
startup_stage = "not_started"
|
telegram_agent.py
CHANGED
|
@@ -41,6 +41,8 @@ PINECONE_INDEX = os.environ.get("PINECONE_INDEX", "jira-agent-memory")
|
|
| 41 |
MCP_SERVER_SCRIPT = os.path.join(os.path.dirname(__file__), "jira_mcp_server.py")
|
| 42 |
EMBED_DIM = 768
|
| 43 |
MODEL_PATH = os.path.join(os.path.dirname(__file__), "models", "microsoft_Phi-4-mini-instruct-Q4_K_M.gguf")
|
|
|
|
|
|
|
| 44 |
|
| 45 |
# ββ Globals (lazy-initialized) ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 46 |
bot: Optional[telebot.TeleBot] = None
|
|
@@ -51,6 +53,7 @@ user_sessions = {}
|
|
| 51 |
loop_ref = None
|
| 52 |
mcp_session_ref = None
|
| 53 |
openai_tools_ref = None
|
|
|
|
| 54 |
|
| 55 |
|
| 56 |
# ββ Lazy initializers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -80,7 +83,7 @@ def embed(text: str) -> list[float]:
|
|
| 80 |
|
| 81 |
|
| 82 |
def _init_llm() -> Llama:
|
| 83 |
-
global llm
|
| 84 |
if llm is not None:
|
| 85 |
return llm
|
| 86 |
|
|
@@ -89,9 +92,10 @@ def _init_llm() -> Llama:
|
|
| 89 |
from huggingface_hub import hf_hub_download
|
| 90 |
print(" Resolving model from HuggingFace cache...\n")
|
| 91 |
cached_path = hf_hub_download(
|
| 92 |
-
repo_id=
|
| 93 |
-
filename=
|
| 94 |
)
|
|
|
|
| 95 |
print(f" Model cached at: {cached_path}")
|
| 96 |
print(f" Loading model...\n")
|
| 97 |
|
|
@@ -108,35 +112,55 @@ def _init_pinecone():
|
|
| 108 |
if pine_index is not None:
|
| 109 |
return pine_index
|
| 110 |
if not PINECONE_API_KEY:
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
return pine_index
|
| 121 |
|
| 122 |
|
| 123 |
def store(user_input, agent_reply, tool_name, tool_args, tool_result):
|
|
|
|
|
|
|
| 124 |
doc = f"User: {user_input}\nAgent: {agent_reply}\nTool: {tool_name}\nArgs: {json.dumps(tool_args)}\nResult: {json.dumps(tool_result)}"
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
|
| 134 |
def recall(user_input: str, top_k: int = 2) -> str:
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
total = stats.get("total_vector_count", 0)
|
| 137 |
if total == 0:
|
| 138 |
return ""
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
chunks = [f"[past]\n{m['metadata'].get('user_input','')} β {m['metadata'].get('tool_name','')}"
|
| 141 |
for m in results.get("matches", []) if m.get('score', 0) > 0.5]
|
| 142 |
return "\nRECENT SIMILAR (context only):\n" + "\n---\n".join(chunks) + "\n" if chunks else ""
|
|
@@ -291,17 +315,28 @@ async def start_telegram_with_mcp():
|
|
| 291 |
global mcp_session_ref, openai_tools_ref, loop_ref
|
| 292 |
loop_ref = asyncio.get_event_loop()
|
| 293 |
|
|
|
|
|
|
|
| 294 |
# Lazy init all components
|
| 295 |
_init_telegram_bot()
|
|
|
|
|
|
|
| 296 |
_init_embedder()
|
|
|
|
|
|
|
| 297 |
_init_llm()
|
|
|
|
|
|
|
| 298 |
_init_pinecone()
|
|
|
|
|
|
|
| 299 |
_register_handlers()
|
| 300 |
|
| 301 |
server_params = StdioServerParameters(command=sys.executable, args=[MCP_SERVER_SCRIPT], env={**os.environ})
|
| 302 |
|
| 303 |
async with stdio_client(server_params) as (read, write):
|
| 304 |
async with ClientSession(read, write) as session:
|
|
|
|
| 305 |
await session.initialize()
|
| 306 |
tools_resp = await session.list_tools()
|
| 307 |
mcp_tools = tools_resp.tools
|
|
@@ -313,10 +348,17 @@ async def start_telegram_with_mcp():
|
|
| 313 |
shared_state.openai_tools = openai_tools
|
| 314 |
shared_state.telegram_bot = bot
|
| 315 |
shared_state.llm_instance = llm
|
|
|
|
|
|
|
| 316 |
mcp_session_ref = session
|
| 317 |
openai_tools_ref = openai_tools
|
| 318 |
|
| 319 |
-
stats =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
print("=" * 60)
|
| 321 |
print(f" Telegram + MCP | Tools: {[t.name for t in mcp_tools]}")
|
| 322 |
print(f" Memory: {stats.get('total_vector_count', 0)} vectors")
|
|
@@ -328,6 +370,7 @@ async def start_telegram_with_mcp():
|
|
| 328 |
|
| 329 |
t = threading.Thread(target=run_telegram, daemon=True)
|
| 330 |
t.start()
|
|
|
|
| 331 |
|
| 332 |
try:
|
| 333 |
while True:
|
|
@@ -335,6 +378,7 @@ async def start_telegram_with_mcp():
|
|
| 335 |
except (KeyboardInterrupt, SystemExit):
|
| 336 |
print("\nShutting down Telegram...")
|
| 337 |
bot.stop_polling()
|
|
|
|
| 338 |
|
| 339 |
|
| 340 |
# ββ Standalone run ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 41 |
MCP_SERVER_SCRIPT = os.path.join(os.path.dirname(__file__), "jira_mcp_server.py")
|
| 42 |
EMBED_DIM = 768
|
| 43 |
MODEL_PATH = os.path.join(os.path.dirname(__file__), "models", "microsoft_Phi-4-mini-instruct-Q4_K_M.gguf")
|
| 44 |
+
MODEL_REPO_ID = os.environ.get("MODEL_REPO_ID", "bartowski/microsoft_Phi-4-mini-instruct-GGUF")
|
| 45 |
+
MODEL_FILENAME = os.environ.get("MODEL_FILENAME", "microsoft_Phi-4-mini-instruct-Q4_K_M.gguf")
|
| 46 |
|
| 47 |
# ββ Globals (lazy-initialized) ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 48 |
bot: Optional[telebot.TeleBot] = None
|
|
|
|
| 53 |
loop_ref = None
|
| 54 |
mcp_session_ref = None
|
| 55 |
openai_tools_ref = None
|
| 56 |
+
resolved_model_path = None
|
| 57 |
|
| 58 |
|
| 59 |
# ββ Lazy initializers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 83 |
|
| 84 |
|
| 85 |
def _init_llm() -> Llama:
|
| 86 |
+
global llm, resolved_model_path
|
| 87 |
if llm is not None:
|
| 88 |
return llm
|
| 89 |
|
|
|
|
| 92 |
from huggingface_hub import hf_hub_download
|
| 93 |
print(" Resolving model from HuggingFace cache...\n")
|
| 94 |
cached_path = hf_hub_download(
|
| 95 |
+
repo_id=MODEL_REPO_ID,
|
| 96 |
+
filename=MODEL_FILENAME,
|
| 97 |
)
|
| 98 |
+
resolved_model_path = cached_path
|
| 99 |
print(f" Model cached at: {cached_path}")
|
| 100 |
print(f" Loading model...\n")
|
| 101 |
|
|
|
|
| 112 |
if pine_index is not None:
|
| 113 |
return pine_index
|
| 114 |
if not PINECONE_API_KEY:
|
| 115 |
+
print(" Pinecone disabled: missing PINECONE_API_KEY.\n")
|
| 116 |
+
return None
|
| 117 |
+
try:
|
| 118 |
+
pc = Pinecone(api_key=PINECONE_API_KEY)
|
| 119 |
+
existing = [i.name for i in pc.list_indexes()]
|
| 120 |
+
if PINECONE_INDEX not in existing:
|
| 121 |
+
print(f" Creating Pinecone index '{PINECONE_INDEX}'...")
|
| 122 |
+
pc.create_index(name=PINECONE_INDEX, dimension=EMBED_DIM, metric="cosine",
|
| 123 |
+
spec=ServerlessSpec(cloud="aws", region="us-east-1"))
|
| 124 |
+
print(" Index created.\n")
|
| 125 |
+
pine_index = pc.Index(PINECONE_INDEX)
|
| 126 |
+
except Exception as e:
|
| 127 |
+
print(f" Pinecone disabled due to init error: {str(e)}\n")
|
| 128 |
+
pine_index = None
|
| 129 |
return pine_index
|
| 130 |
|
| 131 |
|
| 132 |
def store(user_input, agent_reply, tool_name, tool_args, tool_result):
|
| 133 |
+
if pine_index is None or _embedder is None:
|
| 134 |
+
return
|
| 135 |
doc = f"User: {user_input}\nAgent: {agent_reply}\nTool: {tool_name}\nArgs: {json.dumps(tool_args)}\nResult: {json.dumps(tool_result)}"
|
| 136 |
+
try:
|
| 137 |
+
pine_index.upsert(vectors=[{
|
| 138 |
+
"id": str(uuid.uuid4()), "values": embed(doc),
|
| 139 |
+
"metadata": {"doc": doc, "user_input": user_input, "agent_reply": agent_reply,
|
| 140 |
+
"tool_name": tool_name, "tool_args": json.dumps(tool_args),
|
| 141 |
+
"tool_result": json.dumps(tool_result),
|
| 142 |
+
"timestamp": datetime.datetime.utcnow().isoformat()},
|
| 143 |
+
}])
|
| 144 |
+
except Exception as e:
|
| 145 |
+
print(f" Warning: failed to store memory: {str(e)}")
|
| 146 |
|
| 147 |
|
| 148 |
def recall(user_input: str, top_k: int = 2) -> str:
|
| 149 |
+
if pine_index is None or _embedder is None:
|
| 150 |
+
return ""
|
| 151 |
+
try:
|
| 152 |
+
stats = pine_index.describe_index_stats()
|
| 153 |
+
except Exception as e:
|
| 154 |
+
print(f" Warning: failed to query memory stats: {str(e)}")
|
| 155 |
+
return ""
|
| 156 |
total = stats.get("total_vector_count", 0)
|
| 157 |
if total == 0:
|
| 158 |
return ""
|
| 159 |
+
try:
|
| 160 |
+
results = pine_index.query(vector=embed(user_input), top_k=min(top_k, total), include_metadata=True)
|
| 161 |
+
except Exception as e:
|
| 162 |
+
print(f" Warning: failed to query memory: {str(e)}")
|
| 163 |
+
return ""
|
| 164 |
chunks = [f"[past]\n{m['metadata'].get('user_input','')} β {m['metadata'].get('tool_name','')}"
|
| 165 |
for m in results.get("matches", []) if m.get('score', 0) > 0.5]
|
| 166 |
return "\nRECENT SIMILAR (context only):\n" + "\n---\n".join(chunks) + "\n" if chunks else ""
|
|
|
|
| 315 |
global mcp_session_ref, openai_tools_ref, loop_ref
|
| 316 |
loop_ref = asyncio.get_event_loop()
|
| 317 |
|
| 318 |
+
import shared_state
|
| 319 |
+
shared_state.startup_stage = "init_telegram"
|
| 320 |
# Lazy init all components
|
| 321 |
_init_telegram_bot()
|
| 322 |
+
|
| 323 |
+
shared_state.startup_stage = "init_embedder"
|
| 324 |
_init_embedder()
|
| 325 |
+
|
| 326 |
+
shared_state.startup_stage = "init_llm"
|
| 327 |
_init_llm()
|
| 328 |
+
|
| 329 |
+
shared_state.startup_stage = "init_pinecone"
|
| 330 |
_init_pinecone()
|
| 331 |
+
|
| 332 |
+
shared_state.startup_stage = "register_handlers"
|
| 333 |
_register_handlers()
|
| 334 |
|
| 335 |
server_params = StdioServerParameters(command=sys.executable, args=[MCP_SERVER_SCRIPT], env={**os.environ})
|
| 336 |
|
| 337 |
async with stdio_client(server_params) as (read, write):
|
| 338 |
async with ClientSession(read, write) as session:
|
| 339 |
+
shared_state.startup_stage = "init_mcp_session"
|
| 340 |
await session.initialize()
|
| 341 |
tools_resp = await session.list_tools()
|
| 342 |
mcp_tools = tools_resp.tools
|
|
|
|
| 348 |
shared_state.openai_tools = openai_tools
|
| 349 |
shared_state.telegram_bot = bot
|
| 350 |
shared_state.llm_instance = llm
|
| 351 |
+
shared_state.model_path = resolved_model_path or MODEL_PATH
|
| 352 |
+
shared_state.telegram_error = None
|
| 353 |
mcp_session_ref = session
|
| 354 |
openai_tools_ref = openai_tools
|
| 355 |
|
| 356 |
+
stats = {"total_vector_count": 0}
|
| 357 |
+
if pine_index is not None:
|
| 358 |
+
try:
|
| 359 |
+
stats = pine_index.describe_index_stats()
|
| 360 |
+
except Exception as e:
|
| 361 |
+
print(f" Warning: failed to read Pinecone stats: {str(e)}")
|
| 362 |
print("=" * 60)
|
| 363 |
print(f" Telegram + MCP | Tools: {[t.name for t in mcp_tools]}")
|
| 364 |
print(f" Memory: {stats.get('total_vector_count', 0)} vectors")
|
|
|
|
| 370 |
|
| 371 |
t = threading.Thread(target=run_telegram, daemon=True)
|
| 372 |
t.start()
|
| 373 |
+
shared_state.startup_stage = "running"
|
| 374 |
|
| 375 |
try:
|
| 376 |
while True:
|
|
|
|
| 378 |
except (KeyboardInterrupt, SystemExit):
|
| 379 |
print("\nShutting down Telegram...")
|
| 380 |
bot.stop_polling()
|
| 381 |
+
shared_state.startup_stage = "stopped"
|
| 382 |
|
| 383 |
|
| 384 |
# ββ Standalone run ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|