Maheen001 commited on
Commit
df46389
·
verified ·
1 Parent(s): 90b2811

Update agent/agent_core.py

Browse files
Files changed (1) hide show
  1. agent/agent_core.py +211 -188
agent/agent_core.py CHANGED
@@ -1,11 +1,13 @@
1
  """
2
  LifeAdmin AI - Core Agent Logic
3
- Fully compatible with your UI and HuggingFace
4
- Includes:
5
- - process_files_to_rag()
6
- - manual_tool_call()
7
- - execute()
8
- - plan(), reflect(), memory, RAG, MCP
 
 
9
  """
10
 
11
  import asyncio
@@ -14,6 +16,7 @@ import time
14
  from typing import List, Dict, Any, Optional
15
  from dataclasses import dataclass, asdict
16
  from enum import Enum
 
17
 
18
  from agent.mcp_client import MCPClient
19
  from agent.rag_engine import RAGEngine
@@ -21,10 +24,9 @@ from agent.memory import MemoryStore
21
  from utils.llm_utils import get_llm_response
22
 
23
 
24
- # ============================================================
25
- # DATA MODELS
26
- # ============================================================
27
-
28
  class TaskStatus(Enum):
29
  PENDING = "pending"
30
  IN_PROGRESS = "in_progress"
@@ -35,7 +37,7 @@ class TaskStatus(Enum):
35
  @dataclass
36
  class AgentThought:
37
  step: int
38
- type: str # planning, tool_call, reflection, answer
39
  content: str
40
  tool_name: Optional[str] = None
41
  tool_args: Optional[Dict] = None
@@ -58,178 +60,125 @@ class AgentTask:
58
  error: Optional[str] = None
59
 
60
 
61
- # ============================================================
62
- # MAIN AGENT
63
- # ============================================================
64
-
65
  class LifeAdminAgent:
66
-
67
  def __init__(self):
68
  self.mcp_client = MCPClient()
69
  self.rag_engine = RAGEngine()
70
  self.memory = MemoryStore()
71
  self.thoughts: List[AgentThought] = []
72
 
73
- # ----------------------------------------------------
74
- # RESET THOUGHTS
75
- # ----------------------------------------------------
 
76
  def reset(self):
 
77
  self.thoughts = []
78
 
79
- # ----------------------------------------------------
80
- # PROCESS FILES → TO RAG
81
- # ----------------------------------------------------
82
- async def process_files_to_rag(self, files: List[Dict]):
83
- """
84
- Expected format: [{ "path": "...", "name": "..." }]
85
- Your UI calls this after uploads.
86
- """
87
-
88
- for f in files:
89
- try:
90
- await self.rag_engine.add_document(
91
- file_path=f["path"],
92
- metadata={"filename": f["name"]}
93
- )
94
-
95
- self.thoughts.append(AgentThought(
96
- step=len(self.thoughts) + 1,
97
- type="planning",
98
- content=f"Added to RAG: {f['name']}"
99
- ))
100
-
101
- except Exception as e:
102
- self.thoughts.append(AgentThought(
103
- step=len(self.thoughts) + 1,
104
- type="planning",
105
- content=f"Failed indexing: {f['name']}, error={str(e)}"
106
- ))
107
-
108
- return True
109
-
110
- # ----------------------------------------------------
111
- # MANUAL TOOL CALL (Used in Manual Dashboard)
112
- # ----------------------------------------------------
113
- async def manual_tool_call(self, tool: str, args: Dict[str, Any]):
114
- """
115
- Your UI calls this for:
116
- - OCR
117
- - PDF extract
118
- - email draft
119
- - calendar event
120
- - file tools etc.
121
- """
122
-
123
  self.thoughts.append(AgentThought(
124
  step=len(self.thoughts) + 1,
125
- type="tool_call",
126
- content=f"Manual call: {tool}",
127
- tool_name=tool,
128
- tool_args=args
129
- ))
130
-
131
- try:
132
- result = await self.mcp_client.call_tool(tool, args)
133
-
134
- self.thoughts.append(AgentThought(
135
- step=len(self.thoughts) + 1,
136
- type="tool_call",
137
- content="Manual tool execution succeeded",
138
- tool_name=tool,
139
- tool_result=result
140
- ))
141
-
142
- return result
143
-
144
- except Exception as e:
145
- self.thoughts.append(AgentThought(
146
- step=len(self.thoughts) + 1,
147
- type="tool_call",
148
- content=f"Manual tool failed: {str(e)}",
149
- tool_name=tool
150
- ))
151
- return {"error": str(e)}
152
-
153
- # ----------------------------------------------------
154
- # PLAN TASKS
155
- # ----------------------------------------------------
156
- async def plan(self, user_request: str, files: List[str] = None):
157
- self.thoughts.append(AgentThought(
158
- step=len(self.thoughts)+1,
159
  type="planning",
160
- content=f"Analyzing user request: {user_request}"
161
  ))
162
 
163
- tools = await self.mcp_client.list_tools()
164
- tool_desc = "\n".join([f"{t['name']}: {t['description']}" for t in tools])
165
-
166
- rag_docs = await self.rag_engine.search(user_request, k=3)
167
- rag_context = "\n".join([d["text"][:300] for d in rag_docs]) or "None"
 
168
 
169
- mem_context = self.memory.get_relevant_memories(user_request)
 
 
 
 
 
 
170
 
171
- prompt = f"""
172
- You are a task planner. USER REQUEST:
173
- {user_request}
174
 
175
- FILES: {files or []}
176
 
177
- TOOLS AVAILABLE:
 
 
 
 
178
  {tool_desc}
179
-
180
- RAG CONTEXT:
181
  {rag_context}
 
 
182
 
183
- MEMORY:
184
- {mem_context}
 
 
 
185
 
186
- Return JSON ONLY:
187
  [
188
  {{
189
- "id": "task1",
190
- "description": "Extract something",
191
  "tool": "ocr_extract_text",
192
- "args": {{"file_path": "x.pdf"}}
193
  }}
194
  ]
195
  """
196
-
197
- resp = await get_llm_response(prompt, temperature=0.2)
198
- txt = resp.strip()
199
-
200
- if "```json" in txt:
201
- txt = txt.split("```json")[1].split("```")[0].strip()
202
 
203
  try:
204
- parsed = json.loads(txt)
205
- tasks = [AgentTask(**t) for t in parsed]
206
 
207
- except Exception:
 
 
 
 
 
 
 
208
  self.thoughts.append(AgentThought(
209
- step=len(self.thoughts)+1,
210
  type="planning",
211
- content="Planning failed invalid JSON returned by LLM."
 
 
 
 
 
 
 
212
  ))
213
  return []
214
 
 
 
 
 
215
  self.thoughts.append(AgentThought(
216
- step=len(self.thoughts)+1,
217
- type="planning",
218
- content=f"Created {len(tasks)} tasks"
219
- ))
220
- return tasks
221
-
222
- # ----------------------------------------------------
223
- # EXECUTE A SINGLE TASK
224
- # ----------------------------------------------------
225
- async def execute_task(self, task: AgentTask):
226
- self.thoughts.append(AgentThought(
227
- step=len(self.thoughts)+1,
228
  type="tool_call",
229
- content=f"Executing: {task.description}",
230
  tool_name=task.tool,
231
  tool_args=task.args
232
  ))
 
233
 
234
  try:
235
  result = await self.mcp_client.call_tool(task.tool, task.args)
@@ -237,86 +186,160 @@ Return JSON ONLY:
237
  task.status = TaskStatus.COMPLETED
238
 
239
  self.thoughts.append(AgentThought(
240
- step=len(self.thoughts)+1,
241
  type="tool_call",
242
- content=f"Completed {task.description}",
243
  tool_name=task.tool,
244
  tool_result=result
245
  ))
246
-
247
  except Exception as e:
248
  task.status = TaskStatus.FAILED
249
  task.error = str(e)
250
-
251
  self.thoughts.append(AgentThought(
252
- step=len(self.thoughts)+1,
253
  type="tool_call",
254
- content=f"Failed {task.description}: {str(e)}",
255
  tool_name=task.tool
256
  ))
 
257
 
258
- return task
259
-
260
- # ----------------------------------------------------
261
- # REFLECT / SUMMARIZE
262
- # ----------------------------------------------------
263
- async def reflect(self, tasks: List[AgentTask], original: str):
264
  self.thoughts.append(AgentThought(
265
- step=len(self.thoughts)+1,
266
  type="reflection",
267
- content="Summarizing results"
268
  ))
269
 
270
- results_txt = "\n".join([
271
- f"✓ {t.description}: {str(t.result)[:200]}"
272
- if t.status == TaskStatus.COMPLETED
273
- else f" {t.description}: {t.error}"
274
- for t in tasks
275
- ])
276
-
277
- prompt = f"""
278
- Summarize results clearly for a user.
279
-
280
- REQUEST:
281
- {original}
282
-
283
- RESULTS:
284
- {results_txt}
285
  """
286
 
287
- answer = await get_llm_response(prompt, temperature=0.4)
 
 
 
288
 
289
  self.thoughts.append(AgentThought(
290
- step=len(self.thoughts)+1,
291
  type="answer",
292
  content=answer
293
  ))
294
 
295
- self.memory.add_memory(
296
- content=f"Request: {original}\nAnswer: {answer}",
297
- metadata={"timestamp": time.time()}
298
- )
 
 
 
 
 
 
299
  return answer
300
 
301
- # ----------------------------------------------------
302
- # MAIN EXECUTION (Used by Voice Mode)
303
- # ----------------------------------------------------
304
- async def execute(self, user_request: str, files: List[str] = None):
 
 
 
 
305
  self.reset()
306
 
307
  tasks = await self.plan(user_request, files)
308
  if not tasks:
309
- return "I could not create a plan. Try rephrasing.", self.thoughts
 
 
 
 
 
 
310
 
311
  executed = []
312
  for t in tasks:
313
- executed.append(await self.execute_task(t))
 
314
 
315
- final = await self.reflect(executed, user_request)
316
- return final, self.thoughts
317
 
318
- # ----------------------------------------------------
319
- # EXPORT THOUGHT TRACE
320
- # ----------------------------------------------------
321
- def get_thought_trace(self):
322
  return [asdict(t) for t in self.thoughts]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  LifeAdmin AI - Core Agent Logic
3
+ Final stable version (HF / Gradio-compatible).
4
+ Provides:
5
+ - plan()
6
+ - execute_task()
7
+ - reflect()
8
+ - execute() -> (final_answer, thoughts)
9
+ - process_files_to_rag()
10
+ - manual_tool_call()
11
  """
12
 
13
  import asyncio
 
16
  from typing import List, Dict, Any, Optional
17
  from dataclasses import dataclass, asdict
18
  from enum import Enum
19
+ from pathlib import Path
20
 
21
  from agent.mcp_client import MCPClient
22
  from agent.rag_engine import RAGEngine
 
24
  from utils.llm_utils import get_llm_response
25
 
26
 
27
+ # -------------------------
28
+ # Data models
29
+ # -------------------------
 
30
  class TaskStatus(Enum):
31
  PENDING = "pending"
32
  IN_PROGRESS = "in_progress"
 
37
  @dataclass
38
  class AgentThought:
39
  step: int
40
+ type: str # 'planning', 'tool_call', 'reflection', 'answer'
41
  content: str
42
  tool_name: Optional[str] = None
43
  tool_args: Optional[Dict] = None
 
60
  error: Optional[str] = None
61
 
62
 
63
+ # -------------------------
64
+ # LifeAdminAgent
65
+ # -------------------------
 
66
  class LifeAdminAgent:
 
67
  def __init__(self):
68
  self.mcp_client = MCPClient()
69
  self.rag_engine = RAGEngine()
70
  self.memory = MemoryStore()
71
  self.thoughts: List[AgentThought] = []
72
 
73
+ # ensure data directories exist
74
+ Path("data/uploads").mkdir(parents=True, exist_ok=True)
75
+ Path("data/outputs").mkdir(parents=True, exist_ok=True)
76
+
77
  def reset(self):
78
+ """Reset thoughts / context for a new request"""
79
  self.thoughts = []
80
 
81
+ # ---------------------
82
+ # Planning
83
+ # ---------------------
84
+ async def plan(self, user_request: str, files: List[str] = None) -> List[AgentTask]:
85
+ """Create an execution plan (list of AgentTask) using LLM + RAG + memory"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  self.thoughts.append(AgentThought(
87
  step=len(self.thoughts) + 1,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  type="planning",
89
+ content=f"Analyzing request: {user_request}"
90
  ))
91
 
92
+ # list tools
93
+ try:
94
+ tools = await self.mcp_client.list_tools()
95
+ except Exception:
96
+ tools = []
97
+ tool_desc = "\n".join([f"- {t['name']}: {t.get('description','')}" for t in tools]) if tools else "No tool metadata available"
98
 
99
+ # RAG search
100
+ rag_docs = []
101
+ if user_request and user_request.strip():
102
+ try:
103
+ rag_docs = await self.rag_engine.search(user_request, k=3)
104
+ except Exception:
105
+ rag_docs = []
106
 
107
+ rag_context = "\n".join([d.get("text","")[:250] for d in rag_docs]) if rag_docs else "No relevant docs"
 
 
108
 
109
+ memory_context = self.memory.get_relevant_memories(user_request) if self.memory else "No memory"
110
 
111
+ planning_prompt = f"""
112
+ You are an autonomous life admin assistant. Produce a JSON array of tasks (no extra text).
113
+ User request: {user_request}
114
+ Available files: {files or []}
115
+ Available tools:
116
  {tool_desc}
117
+ RAG context:
 
118
  {rag_context}
119
+ Memory:
120
+ {memory_context}
121
 
122
+ Return ONLY valid JSON array of tasks. Each task must contain:
123
+ - id (string)
124
+ - description (string)
125
+ - tool (one of the tool names)
126
+ - args (a JSON object)
127
 
128
+ Example:
129
  [
130
  {{
131
+ "id": "task_1",
132
+ "description": "Extract text from invoice.pdf",
133
  "tool": "ocr_extract_text",
134
+ "args": {{"file_path": "data/uploads/invoice.pdf", "language": "en"}}
135
  }}
136
  ]
137
  """
138
+ self.thoughts.append(AgentThought(
139
+ step=len(self.thoughts) + 1,
140
+ type="planning",
141
+ content="Asking LLM to create a plan..."
142
+ ))
 
143
 
144
  try:
145
+ plan_text = await get_llm_response(planning_prompt, temperature=0.2)
146
+ plan_text = plan_text.strip()
147
 
148
+ # try to extract JSON if wrapped in code fences
149
+ if "```json" in plan_text:
150
+ plan_text = plan_text.split("```json", 1)[1].split("```", 1)[0].strip()
151
+ elif "```" in plan_text:
152
+ plan_text = plan_text.split("```", 1)[1].split("```", 1)[0].strip()
153
+
154
+ tasks_data = json.loads(plan_text)
155
+ tasks = [AgentTask(**t) for t in tasks_data]
156
  self.thoughts.append(AgentThought(
157
+ step=len(self.thoughts) + 1,
158
  type="planning",
159
+ content=f"Plan created with {len(tasks)} tasks."
160
+ ))
161
+ return tasks
162
+ except Exception as e:
163
+ self.thoughts.append(AgentThought(
164
+ step=len(self.thoughts) + 1,
165
+ type="planning",
166
+ content=f"Planning failed: {str(e)}"
167
  ))
168
  return []
169
 
170
+ # ---------------------
171
+ # Execution of a single task
172
+ # ---------------------
173
+ async def execute_task(self, task: AgentTask) -> AgentTask:
174
  self.thoughts.append(AgentThought(
175
+ step=len(self.thoughts) + 1,
 
 
 
 
 
 
 
 
 
 
 
176
  type="tool_call",
177
+ content=f"Executing task: {task.description}",
178
  tool_name=task.tool,
179
  tool_args=task.args
180
  ))
181
+ task.status = TaskStatus.IN_PROGRESS
182
 
183
  try:
184
  result = await self.mcp_client.call_tool(task.tool, task.args)
 
186
  task.status = TaskStatus.COMPLETED
187
 
188
  self.thoughts.append(AgentThought(
189
+ step=len(self.thoughts) + 1,
190
  type="tool_call",
191
+ content=f"Completed: {task.description}",
192
  tool_name=task.tool,
193
  tool_result=result
194
  ))
195
+ return task
196
  except Exception as e:
197
  task.status = TaskStatus.FAILED
198
  task.error = str(e)
 
199
  self.thoughts.append(AgentThought(
200
+ step=len(self.thoughts) + 1,
201
  type="tool_call",
202
+ content=f"Failed: {task.description} - {str(e)}",
203
  tool_name=task.tool
204
  ))
205
+ return task
206
 
207
+ # ---------------------
208
+ # Reflection / final answer
209
+ # ---------------------
210
+ async def reflect(self, tasks: List[AgentTask], original_request: str) -> str:
 
 
211
  self.thoughts.append(AgentThought(
212
+ step=len(self.thoughts) + 1,
213
  type="reflection",
214
+ content="Synthesizing results..."
215
  ))
216
 
217
+ summary_lines = []
218
+ for t in tasks:
219
+ if t.status == TaskStatus.COMPLETED:
220
+ summary_lines.append(f" {t.description}: {str(t.result)[:300]}")
221
+ else:
222
+ summary_lines.append(f"✗ {t.description}: {t.error}")
223
+
224
+ reflection_prompt = f"""
225
+ You are the agent summarizing execution results.
226
+ Original request: {original_request}
227
+ Execution summary:
228
+ {chr(10).join(summary_lines)}
229
+
230
+ Write a clear, friendly reply telling the user what was done, outputs created, any errors, and next steps.
 
231
  """
232
 
233
+ try:
234
+ answer = await get_llm_response(reflection_prompt, temperature=0.5)
235
+ except Exception as e:
236
+ answer = f"Reflection failed: {str(e)}"
237
 
238
  self.thoughts.append(AgentThought(
239
+ step=len(self.thoughts) + 1,
240
  type="answer",
241
  content=answer
242
  ))
243
 
244
+ # store short memory
245
+ try:
246
+ self.memory.add_memory(
247
+ content=f"Request: {original_request}\nResult: {answer}",
248
+ memory_type="task_completion",
249
+ metadata={"timestamp": time.time()}
250
+ )
251
+ except Exception:
252
+ pass
253
+
254
  return answer
255
 
256
+ # ---------------------
257
+ # Main execute (no streaming)
258
+ # ---------------------
259
+ async def execute(self, user_request: str, files: List[str] = None) -> (str, List[AgentThought]):
260
+ """
261
+ Run plan -> execute each task -> reflect
262
+ Returns: (final_answer, list_of_thoughts)
263
+ """
264
  self.reset()
265
 
266
  tasks = await self.plan(user_request, files)
267
  if not tasks:
268
+ err_msg = "Could not create an execution plan. Try rephrasing your request."
269
+ self.thoughts.append(AgentThought(
270
+ step=len(self.thoughts) + 1,
271
+ type="answer",
272
+ content=err_msg
273
+ ))
274
+ return err_msg, self.thoughts
275
 
276
  executed = []
277
  for t in tasks:
278
+ executed_task = await self.execute_task(t)
279
+ executed.append(executed_task)
280
 
281
+ final_answer = await self.reflect(executed, user_request)
282
+ return final_answer, self.thoughts
283
 
284
+ # ---------------------
285
+ # Utility: provide thought trace for UI
286
+ # ---------------------
287
+ def get_thought_trace(self) -> List[Dict[str, Any]]:
288
  return [asdict(t) for t in self.thoughts]
289
+
290
+ # ---------------------
291
+ # Add uploaded files into RAG index (helper used by UI)
292
+ # ---------------------
293
+ async def process_files_to_rag(self, files: List[Dict[str, str]]):
294
+ """
295
+ files: list of dicts {'path': <path>, 'name': <filename>}
296
+ Extract text using available local tools (pdf/text/ocr) and add to RAG.
297
+ """
298
+ for file_info in files:
299
+ path = file_info.get("path")
300
+ name = file_info.get("name", Path(path).name if path else "")
301
+ try:
302
+ text = ""
303
+ if path and path.lower().endswith(".pdf"):
304
+ # try utils.pdf_utils
305
+ try:
306
+ from utils.pdf_utils import extract_text_from_pdf
307
+ text = extract_text_from_pdf(path)
308
+ except Exception:
309
+ text = ""
310
+ elif path and path.lower().endswith((".png", ".jpg", ".jpeg", ".tiff")):
311
+ # use MCP OCR tool (via client) or local easyocr
312
+ try:
313
+ result = await self.mcp_client.call_tool("ocr_extract_text", {"file_path": path, "language": "en"})
314
+ text = result.get("text", "")
315
+ except Exception:
316
+ text = ""
317
+ else:
318
+ # read plain text files
319
+ try:
320
+ with open(path, "r", encoding="utf-8") as f:
321
+ text = f.read()
322
+ except Exception:
323
+ text = ""
324
+
325
+ if text and len(text.strip()) > 20:
326
+ try:
327
+ await self.rag_engine.add_document(text=text, metadata={"filename": name, "path": path})
328
+ except Exception:
329
+ pass
330
+ except Exception:
331
+ continue
332
+
333
+ # ---------------------
334
+ # Manual tool call wrapper for UI (guarantees consistent return shape)
335
+ # ---------------------
336
+ async def manual_tool_call(self, tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
337
+ """
338
+ Calls an MCP tool (via MCPClient). Returns dict:
339
+ {'success': bool, 'result': <tool_result> or None, 'error': <err_msg> or None}
340
+ """
341
+ try:
342
+ result = await self.mcp_client.call_tool(tool_name, args)
343
+ return {"success": True, "result": result, "error": None}
344
+ except Exception as e:
345
+ return {"success": False, "result": None, "error": str(e)}