PYAE1994 commited on
Commit
cb662f9
Β·
verified Β·
1 Parent(s): e9e7ef1

Fix: update core/agent.py

Browse files
Files changed (1) hide show
  1. core/agent.py +221 -40
core/agent.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
- Agent Core β€” Uses LLMRouter for ALL LLM calls (no direct API calls)
3
- Planner + Executor + Self-Heal Loop
4
  """
5
 
6
  import asyncio
@@ -9,18 +9,21 @@ import os
9
  import time
10
  from typing import Any, Dict, List, Optional
11
 
 
12
  import structlog
13
 
14
  from core.models import TaskPlan, TaskStep
15
  from api.websocket_manager import WebSocketManager
16
  from memory.db import save_memory, get_history, search_memory
17
- from ai_router.router import LLMRouter
18
 
19
  log = structlog.get_logger()
20
 
21
- DEFAULT_MODEL = os.environ.get("DEFAULT_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct")
 
 
22
  OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
23
 
 
24
  SYSTEM_PROMPT = """You are an elite autonomous AI software engineer β€” like Devin or Manus.
25
  You can plan, code, debug, refactor, test, and deploy software autonomously.
26
  You think step-by-step, write production-quality code, and self-heal on errors.
@@ -30,18 +33,18 @@ Always respond in structured JSON when asked for plans or structured output.
30
  PLANNER_PROMPT = """You are a senior software architect. Given a goal, produce a detailed execution plan.
31
 
32
  Respond ONLY with valid JSON:
33
- {{
34
  "steps": [
35
- {{
36
  "name": "Step name",
37
  "description": "What this step does",
38
  "tool": "code|shell|file|browser|github|memory|search|test|none",
39
  "estimated_seconds": 10
40
- }}
41
  ],
42
  "estimated_duration": 60,
43
  "tools_needed": ["code", "shell"]
44
- }}
45
 
46
  Goal: {goal}
47
  Context: {context}
@@ -51,10 +54,9 @@ Context: {context}
51
  class AgentCore:
52
  def __init__(self, ws_manager: WebSocketManager):
53
  self.ws = ws_manager
54
- self.router = LLMRouter(ws_manager)
55
  self.model = DEFAULT_MODEL
56
 
57
- # ─── LLM Call (delegates to router) ───────────────────────────────────────
58
 
59
  async def llm_stream(
60
  self,
@@ -65,35 +67,180 @@ class AgentCore:
65
  temperature: float = 0.7,
66
  max_tokens: int = 4096,
67
  ) -> str:
68
- """All LLM calls go through LLMRouter β€” no direct API calls."""
69
- return await self.router.ask(
70
- messages=messages,
71
- task_id=task_id,
72
- session_id=session_id,
73
- temperature=temperature,
74
- max_tokens=max_tokens,
75
- model=model or self.model,
76
- stream=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  # ─── Planning ──────────────────────────────────────────────────────────────
80
 
81
  async def plan(self, goal: str, task_id: str, session_id: str = "") -> TaskPlan:
82
- """Generate a structured execution plan via LLMRouter."""
 
83
  memories = await search_memory(goal[:50], session_id=session_id)
84
  context = "\n".join([m["content"][:200] for m in memories[:3]])
85
 
86
  prompt = PLANNER_PROMPT.format(goal=goal, context=context or "No prior context")
 
87
  messages = [
88
  {"role": "system", "content": SYSTEM_PROMPT},
89
- {"role": "user", "content": prompt},
90
  ]
91
 
 
 
 
 
92
  raw = await self.llm_stream(messages, task_id=task_id, session_id=session_id)
93
 
 
94
  try:
 
95
  start = raw.find("{")
96
- end = raw.rfind("}") + 1
97
  if start >= 0 and end > start:
98
  data = json.loads(raw[start:end])
99
  else:
@@ -118,14 +265,20 @@ class AgentCore:
118
  return self._demo_plan(goal)
119
 
120
  def _demo_plan(self, goal: str) -> TaskPlan:
 
121
  steps = [
122
  TaskStep(name="Analyze Requirements", description=f"Analyze: {goal[:60]}", tool="none"),
123
- TaskStep(name="Design Solution", description="Design the solution architecture", tool="none"),
124
- TaskStep(name="Implement", description="Write the implementation code", tool="code"),
125
- TaskStep(name="Test", description="Test the implementation", tool="test"),
126
- TaskStep(name="Document", description="Write documentation", tool="none"),
127
  ]
128
- return TaskPlan(goal=goal, steps=steps, estimated_duration=120, tools_needed=["code", "test"])
 
 
 
 
 
129
 
130
  # ─── Step Execution ────────────────────────────────────────────────────────
131
 
@@ -136,6 +289,7 @@ class AgentCore:
136
  session_id: str = "",
137
  context: Dict = {},
138
  ) -> str:
 
139
  from tools.executor import ToolExecutor
140
  executor = ToolExecutor(self.ws)
141
 
@@ -155,14 +309,18 @@ class AgentCore:
155
  session_id=session_id,
156
  )
157
  await self.ws.emit(task_id, "tool_result", {
158
- "tool": step.tool, "step": step.name,
159
- "result": str(result)[:500], "success": True,
 
 
160
  }, session_id=session_id)
161
  return result
162
  except Exception as e:
163
  await self.ws.emit(task_id, "tool_result", {
164
- "tool": step.tool, "step": step.name,
165
- "error": str(e), "success": False,
 
 
166
  }, session_id=session_id)
167
  return f"Error in {step.name}: {str(e)}"
168
 
@@ -176,11 +334,15 @@ class AgentCore:
176
  task_id: str,
177
  session_id: str = "",
178
  ) -> str:
179
- steps_summary = "\n".join([f"- {s.name}: {r[:200]}" for s, r in zip(steps, results)])
 
 
 
180
  messages = [
181
  {"role": "system", "content": SYSTEM_PROMPT},
182
  {"role": "user", "content": (
183
- f"Summarize the completion of this goal:\nGoal: {goal}\n\n"
 
184
  f"Steps completed:\n{steps_summary}\n\n"
185
  f"Write a concise success summary with key outcomes."
186
  )},
@@ -191,21 +353,40 @@ class AgentCore:
191
  # ─── Chat ──────────────────────────────────────────────────────────────────
192
 
193
  async def stream_chat(self, session_id: str, user_message: str):
194
- await save_memory(content=user_message, memory_type="conversation",
195
- session_id=session_id, key="user_message")
 
 
 
 
 
 
196
 
197
- history = await get_history(session_id, limit=10)
 
198
  messages = [{"role": "system", "content": SYSTEM_PROMPT}]
199
  for h in reversed(history[-10:]):
200
  messages.append({"role": "user", "content": h["content"]})
 
201
  messages.append({"role": "user", "content": user_message})
202
 
203
- await self.ws.emit_chat(session_id, "stream_start", {"status": "generating"})
 
 
 
204
  response = await self.llm_stream(messages, session_id=session_id)
205
 
206
- await save_memory(content=response, memory_type="conversation",
207
- session_id=session_id, key="assistant_response")
 
 
 
 
 
 
208
  await self.ws.emit_chat(session_id, "stream_end", {
209
- "full_response": response, "status": "complete"
 
210
  })
 
211
  return response
 
1
  """
2
+ Agent Core β€” Planner + Executor + Self-Heal Loop
3
+ LLM-powered with OpenAI/Anthropic support, streaming tokens
4
  """
5
 
6
  import asyncio
 
9
  import time
10
  from typing import Any, Dict, List, Optional
11
 
12
+ import httpx
13
  import structlog
14
 
15
  from core.models import TaskPlan, TaskStep
16
  from api.websocket_manager import WebSocketManager
17
  from memory.db import save_memory, get_history, search_memory
 
18
 
19
  log = structlog.get_logger()
20
 
21
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
22
+ ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
23
+ DEFAULT_MODEL = os.environ.get("DEFAULT_MODEL", "gpt-4o")
24
  OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
25
 
26
+
27
  SYSTEM_PROMPT = """You are an elite autonomous AI software engineer β€” like Devin or Manus.
28
  You can plan, code, debug, refactor, test, and deploy software autonomously.
29
  You think step-by-step, write production-quality code, and self-heal on errors.
 
33
  PLANNER_PROMPT = """You are a senior software architect. Given a goal, produce a detailed execution plan.
34
 
35
  Respond ONLY with valid JSON:
36
+ {
37
  "steps": [
38
+ {
39
  "name": "Step name",
40
  "description": "What this step does",
41
  "tool": "code|shell|file|browser|github|memory|search|test|none",
42
  "estimated_seconds": 10
43
+ }
44
  ],
45
  "estimated_duration": 60,
46
  "tools_needed": ["code", "shell"]
47
+ }
48
 
49
  Goal: {goal}
50
  Context: {context}
 
54
  class AgentCore:
55
  def __init__(self, ws_manager: WebSocketManager):
56
  self.ws = ws_manager
 
57
  self.model = DEFAULT_MODEL
58
 
59
+ # ─── LLM Call (with streaming) ─────────────────────────────────────────────
60
 
61
  async def llm_stream(
62
  self,
 
67
  temperature: float = 0.7,
68
  max_tokens: int = 4096,
69
  ) -> str:
70
+ """Stream LLM tokens, emitting llm_chunk events via WebSocket."""
71
+ model = model or self.model
72
+ full_text = ""
73
+
74
+ if OPENAI_API_KEY:
75
+ full_text = await self._openai_stream(
76
+ messages, task_id, session_id, model, temperature, max_tokens
77
+ )
78
+ elif ANTHROPIC_API_KEY:
79
+ full_text = await self._anthropic_stream(
80
+ messages, task_id, session_id, temperature, max_tokens
81
+ )
82
+ else:
83
+ # Demo mode β€” simulate streaming
84
+ full_text = await self._demo_stream(messages, task_id, session_id)
85
+
86
+ return full_text
87
+
88
+ async def _openai_stream(
89
+ self, messages, task_id, session_id, model, temperature, max_tokens
90
+ ) -> str:
91
+ full_text = ""
92
+ headers = {
93
+ "Authorization": f"Bearer {OPENAI_API_KEY}",
94
+ "Content-Type": "application/json",
95
+ }
96
+ payload = {
97
+ "model": model,
98
+ "messages": messages,
99
+ "stream": True,
100
+ "temperature": temperature,
101
+ "max_tokens": max_tokens,
102
+ }
103
+ async with httpx.AsyncClient(timeout=120) as client:
104
+ async with client.stream(
105
+ "POST", f"{OPENAI_BASE_URL}/chat/completions",
106
+ headers=headers, json=payload
107
+ ) as resp:
108
+ resp.raise_for_status()
109
+ async for line in resp.aiter_lines():
110
+ if not line.startswith("data:"):
111
+ continue
112
+ chunk = line[6:].strip()
113
+ if chunk == "[DONE]":
114
+ break
115
+ try:
116
+ data = json.loads(chunk)
117
+ delta = data["choices"][0]["delta"].get("content", "")
118
+ if delta:
119
+ full_text += delta
120
+ if task_id:
121
+ await self.ws.emit(task_id, "llm_chunk", {
122
+ "chunk": delta,
123
+ "accumulated": len(full_text),
124
+ }, session_id=session_id)
125
+ if session_id and not task_id:
126
+ await self.ws.emit_chat(session_id, "llm_chunk", {
127
+ "chunk": delta,
128
+ })
129
+ except Exception:
130
+ pass
131
+ return full_text
132
+
133
+ async def _anthropic_stream(
134
+ self, messages, task_id, session_id, temperature, max_tokens
135
+ ) -> str:
136
+ full_text = ""
137
+ system = ""
138
+ filtered = []
139
+ for m in messages:
140
+ if m["role"] == "system":
141
+ system = m["content"]
142
+ else:
143
+ filtered.append(m)
144
+ headers = {
145
+ "x-api-key": ANTHROPIC_API_KEY,
146
+ "anthropic-version": "2023-06-01",
147
+ "Content-Type": "application/json",
148
+ }
149
+ payload = {
150
+ "model": "claude-3-5-sonnet-20241022",
151
+ "max_tokens": max_tokens,
152
+ "messages": filtered,
153
+ "stream": True,
154
+ }
155
+ if system:
156
+ payload["system"] = system
157
+ async with httpx.AsyncClient(timeout=120) as client:
158
+ async with client.stream(
159
+ "POST", "https://api.anthropic.com/v1/messages",
160
+ headers=headers, json=payload
161
+ ) as resp:
162
+ resp.raise_for_status()
163
+ async for line in resp.aiter_lines():
164
+ if not line.startswith("data:"):
165
+ continue
166
+ try:
167
+ data = json.loads(line[5:].strip())
168
+ if data.get("type") == "content_block_delta":
169
+ delta = data["delta"].get("text", "")
170
+ if delta:
171
+ full_text += delta
172
+ if task_id:
173
+ await self.ws.emit(task_id, "llm_chunk", {
174
+ "chunk": delta,
175
+ }, session_id=session_id)
176
+ if session_id and not task_id:
177
+ await self.ws.emit_chat(session_id, "llm_chunk", {
178
+ "chunk": delta,
179
+ })
180
+ except Exception:
181
+ pass
182
+ return full_text
183
+
184
+ async def _demo_stream(self, messages, task_id, session_id) -> str:
185
+ """Demo mode β€” simulate LLM streaming without API key."""
186
+ last_user = next(
187
+ (m["content"] for m in reversed(messages) if m["role"] == "user"), "Hello"
188
+ )
189
+ response = (
190
+ f"πŸ€– **Devin Agent** (Demo Mode)\n\n"
191
+ f"I received your request: *{last_user[:100]}*\n\n"
192
+ f"To enable real AI responses, set `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` in your environment.\n\n"
193
+ f"**What I can do with a real API key:**\n"
194
+ f"- πŸ“‹ Generate detailed execution plans\n"
195
+ f"- πŸ’» Write and execute code autonomously\n"
196
+ f"- πŸ”§ Debug and self-heal on errors\n"
197
+ f"- πŸ™ Manage GitHub repos autonomously\n"
198
+ f"- 🧠 Remember long-running project context\n"
199
+ f"- πŸš€ Deploy applications automatically\n"
200
  )
201
+ full_text = ""
202
+ for word in response.split():
203
+ chunk = word + " "
204
+ full_text += chunk
205
+ await asyncio.sleep(0.03)
206
+ if task_id:
207
+ await self.ws.emit(task_id, "llm_chunk", {
208
+ "chunk": chunk,
209
+ "demo": True,
210
+ }, session_id=session_id)
211
+ if session_id and not task_id:
212
+ await self.ws.emit_chat(session_id, "llm_chunk", {
213
+ "chunk": chunk,
214
+ "demo": True,
215
+ })
216
+ return full_text
217
 
218
  # ─── Planning ──────────────────────────────────────────────────────────────
219
 
220
  async def plan(self, goal: str, task_id: str, session_id: str = "") -> TaskPlan:
221
+ """Generate a structured execution plan."""
222
+ # Get context from memory
223
  memories = await search_memory(goal[:50], session_id=session_id)
224
  context = "\n".join([m["content"][:200] for m in memories[:3]])
225
 
226
  prompt = PLANNER_PROMPT.format(goal=goal, context=context or "No prior context")
227
+
228
  messages = [
229
  {"role": "system", "content": SYSTEM_PROMPT},
230
+ {"role": "user", "content": prompt},
231
  ]
232
 
233
+ if not OPENAI_API_KEY and not ANTHROPIC_API_KEY:
234
+ # Demo plan
235
+ return self._demo_plan(goal)
236
+
237
  raw = await self.llm_stream(messages, task_id=task_id, session_id=session_id)
238
 
239
+ # Extract JSON from response
240
  try:
241
+ # Find JSON block
242
  start = raw.find("{")
243
+ end = raw.rfind("}") + 1
244
  if start >= 0 and end > start:
245
  data = json.loads(raw[start:end])
246
  else:
 
265
  return self._demo_plan(goal)
266
 
267
  def _demo_plan(self, goal: str) -> TaskPlan:
268
+ """Fallback plan for demo mode."""
269
  steps = [
270
  TaskStep(name="Analyze Requirements", description=f"Analyze: {goal[:60]}", tool="none"),
271
+ TaskStep(name="Design Solution", description="Design the solution architecture", tool="none"),
272
+ TaskStep(name="Implement", description="Write the implementation code", tool="code"),
273
+ TaskStep(name="Test", description="Test the implementation", tool="test"),
274
+ TaskStep(name="Document", description="Write documentation", tool="none"),
275
  ]
276
+ return TaskPlan(
277
+ goal=goal,
278
+ steps=steps,
279
+ estimated_duration=120,
280
+ tools_needed=["code", "test"],
281
+ )
282
 
283
  # ─── Step Execution ────────────────────────────────────────────────────────
284
 
 
289
  session_id: str = "",
290
  context: Dict = {},
291
  ) -> str:
292
+ """Execute a single step using the appropriate tool."""
293
  from tools.executor import ToolExecutor
294
  executor = ToolExecutor(self.ws)
295
 
 
309
  session_id=session_id,
310
  )
311
  await self.ws.emit(task_id, "tool_result", {
312
+ "tool": step.tool,
313
+ "step": step.name,
314
+ "result": str(result)[:500],
315
+ "success": True,
316
  }, session_id=session_id)
317
  return result
318
  except Exception as e:
319
  await self.ws.emit(task_id, "tool_result", {
320
+ "tool": step.tool,
321
+ "step": step.name,
322
+ "error": str(e),
323
+ "success": False,
324
  }, session_id=session_id)
325
  return f"Error in {step.name}: {str(e)}"
326
 
 
334
  task_id: str,
335
  session_id: str = "",
336
  ) -> str:
337
+ """Compile final result summary."""
338
+ steps_summary = "\n".join([
339
+ f"- {s.name}: {r[:200]}" for s, r in zip(steps, results)
340
+ ])
341
  messages = [
342
  {"role": "system", "content": SYSTEM_PROMPT},
343
  {"role": "user", "content": (
344
+ f"Summarize the completion of this goal:\n"
345
+ f"Goal: {goal}\n\n"
346
  f"Steps completed:\n{steps_summary}\n\n"
347
  f"Write a concise success summary with key outcomes."
348
  )},
 
353
  # ─── Chat ──────────────────────────────────────────────────────────────────
354
 
355
  async def stream_chat(self, session_id: str, user_message: str):
356
+ """Stream a conversational chat response."""
357
+ # Save user message to memory
358
+ await save_memory(
359
+ content=user_message,
360
+ memory_type="conversation",
361
+ session_id=session_id,
362
+ key="user_message",
363
+ )
364
 
365
+ # Get conversation history
366
+ history = await get_history(session_id, limit=10)
367
  messages = [{"role": "system", "content": SYSTEM_PROMPT}]
368
  for h in reversed(history[-10:]):
369
  messages.append({"role": "user", "content": h["content"]})
370
+
371
  messages.append({"role": "user", "content": user_message})
372
 
373
+ await self.ws.emit_chat(session_id, "stream_start", {
374
+ "status": "generating",
375
+ })
376
+
377
  response = await self.llm_stream(messages, session_id=session_id)
378
 
379
+ # Save assistant response to memory
380
+ await save_memory(
381
+ content=response,
382
+ memory_type="conversation",
383
+ session_id=session_id,
384
+ key="assistant_response",
385
+ )
386
+
387
  await self.ws.emit_chat(session_id, "stream_end", {
388
+ "full_response": response,
389
+ "status": "complete",
390
  })
391
+
392
  return response