Rohan03 commited on
Commit
1640a62
·
verified ·
1 Parent(s): 4daa607

feat: Easy API — purpose(), Team, quickstart wizard, template auto-detection

Browse files
Files changed (1) hide show
  1. purpose_agent/easy.py +531 -0
purpose_agent/easy.py ADDED
@@ -0,0 +1,531 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Easy API — Build self-improving AI agent teams with zero technical expertise.
3
+
4
+ This module is the ONLY thing a non-technical user needs to touch.
5
+ Everything else is auto-configured.
6
+
7
+ import purpose_agent as pa
8
+
9
+ # One line. That's it.
10
+ team = pa.purpose("Build a research assistant that finds and summarizes papers")
11
+ result = team.run("Find recent papers on climate change solutions")
12
+ print(result)
13
+
14
+ Three levels of usage:
15
+ Level 1 (Beginner): pa.purpose("description") → working team
16
+ Level 2 (Intermediate): pa.Team.build(agents=[...]) → custom team
17
+ Level 3 (Advanced): pa.Agent(), pa.Graph(), pa.Conversation() → full control
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ import os
24
+ import re
25
+ from typing import Any
26
+
27
+ from purpose_agent.unified import Agent, Graph, Conversation, parallel, KnowledgeStore, START, END
28
+ from purpose_agent.tools import (
29
+ Tool, FunctionTool, CalculatorTool, PythonExecTool,
30
+ ReadFileTool, WriteFileTool, ToolRegistry,
31
+ )
32
+ from purpose_agent.llm_backend import LLMBackend, MockLLMBackend, ChatMessage
33
+ from purpose_agent.types import State
34
+ from purpose_agent.orchestrator import TaskResult
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ # ═══════════════════════════════════════════════════════════════════════════
40
+ # LEVEL 1 — The purpose() function. ONE call does everything.
41
+ # ═══════════════════════════════════════════════════════════════════════════
42
+
43
+ def purpose(
44
+ description: str,
45
+ model: str | LLMBackend | None = None,
46
+ local: bool = True,
47
+ knowledge: list[str] | str | None = None,
48
+ tools: list[Tool] | None = None,
49
+ interactive: bool = False,
50
+ ) -> "Team":
51
+ """
52
+ Build a complete self-improving agent team from a plain English description.
53
+
54
+ This is the entry point for everyone. No technical knowledge required.
55
+
56
+ Args:
57
+ description: What you want the team to do, in plain English.
58
+ e.g. "Research and summarize scientific papers"
59
+ e.g. "Write Python code and test it"
60
+ e.g. "Analyze CSV files and create reports"
61
+
62
+ model: (optional) Which AI model to use.
63
+ - None → auto-detects (Ollama if installed, else mock for testing)
64
+ - "qwen3:1.7b" → local model via Ollama (free, private)
65
+ - "gpt-4o" → OpenAI (needs OPENAI_API_KEY)
66
+ - "Qwen/Qwen3-32B" → HuggingFace cloud (needs HF_TOKEN)
67
+
68
+ local: If True (default), prefer local models. Zero cost, full privacy.
69
+
70
+ knowledge: (optional) Give your agents knowledge.
71
+ - List of strings: ["fact 1", "fact 2", ...]
72
+ - File path: "./docs" or "./data.txt"
73
+
74
+ tools: (optional) Extra tools for the agents to use.
75
+
76
+ interactive: If True, agents ask for your approval before acting.
77
+
78
+ Returns:
79
+ A Team you can run tasks on. It gets smarter with every task.
80
+
81
+ Examples:
82
+ # Simplest possible usage
83
+ team = purpose("Help me with coding tasks")
84
+ result = team.run("Write a function to sort a list")
85
+ print(result)
86
+
87
+ # With local SLM (free, private)
88
+ team = purpose("Research assistant", model="qwen3:1.7b")
89
+ result = team.run("What are the latest trends in AI?")
90
+
91
+ # With knowledge base
92
+ team = purpose("Answer questions about my docs", knowledge="./my_docs/")
93
+ result = team.run("What is our refund policy?")
94
+
95
+ # Interactive mode (approve each action)
96
+ team = purpose("File organizer", interactive=True)
97
+ result = team.run("Organize my downloads folder")
98
+ """
99
+ # Auto-detect the best model
100
+ resolved_model = _auto_detect_model(model, local)
101
+
102
+ # Analyze the purpose description to pick the right team template
103
+ template = _analyze_purpose(description)
104
+
105
+ # Build tools list
106
+ all_tools = list(tools or [])
107
+
108
+ # Add template-specific tools
109
+ for tool_cls in template["tools"]:
110
+ all_tools.append(tool_cls())
111
+
112
+ # Add knowledge store if provided
113
+ kb = None
114
+ if knowledge:
115
+ kb = _build_knowledge_store(knowledge)
116
+ all_tools.append(kb.as_tool())
117
+
118
+ # Build the team
119
+ team = Team(
120
+ purpose=description,
121
+ agents=template["agents"],
122
+ model=resolved_model,
123
+ tools=all_tools,
124
+ interactive=interactive,
125
+ knowledge=kb,
126
+ )
127
+
128
+ logger.info(f"✅ Team created: {template['name']} ({len(template['agents'])} agents)")
129
+ return team
130
+
131
+
132
+ # ══════════════════════════════════════════════════════════════��════════════
133
+ # LEVEL 2 — Team class. Customizable but still simple.
134
+ # ═══════════════════════════════════════════════════════════════════════════
135
+
136
+ class Team:
137
+ """
138
+ A self-improving team of AI agents.
139
+
140
+ Created automatically by purpose(), or build your own:
141
+
142
+ team = Team.build(
143
+ purpose="code review assistant",
144
+ agents=["researcher", "coder", "reviewer"],
145
+ model="qwen3:1.7b",
146
+ )
147
+ result = team.run("Review this pull request: ...")
148
+ """
149
+
150
+ def __init__(
151
+ self,
152
+ purpose: str,
153
+ agents: list[dict[str, str]],
154
+ model: str | LLMBackend | None = None,
155
+ tools: list[Tool] | None = None,
156
+ interactive: bool = False,
157
+ knowledge: KnowledgeStore | None = None,
158
+ ):
159
+ self.purpose = purpose
160
+ self.interactive = interactive
161
+ self.knowledge = knowledge
162
+ self._history: list[dict] = []
163
+
164
+ # Create Agent instances
165
+ self._agents: list[Agent] = []
166
+ first_agent = None
167
+ for spec in agents:
168
+ agent = Agent(
169
+ name=spec["name"],
170
+ instructions=spec.get("role", ""),
171
+ model=model,
172
+ tools=tools,
173
+ handoff_from=first_agent, # Shared learning!
174
+ )
175
+ self._agents.append(agent)
176
+ if first_agent is None:
177
+ first_agent = agent
178
+
179
+ # Create conversation for multi-agent collaboration
180
+ if len(self._agents) > 1:
181
+ self._conversation = Conversation(self._agents)
182
+ else:
183
+ self._conversation = None
184
+
185
+ def run(self, task: str, verbose: bool = True) -> str:
186
+ """
187
+ Run a task. Returns a human-readable result string.
188
+
189
+ The team gets smarter with every task you give it.
190
+
191
+ Args:
192
+ task: What you want done, in plain English.
193
+ verbose: If True, print progress as it happens.
194
+
195
+ Returns:
196
+ The result as a simple string.
197
+ """
198
+ if verbose:
199
+ print(f"\n🚀 Working on: {task}")
200
+ print(f" Team: {', '.join(a.name for a in self._agents)}")
201
+ print(f" Purpose: {self.purpose}")
202
+ print()
203
+
204
+ # Single agent → direct run
205
+ if len(self._agents) == 1:
206
+ result = self._agents[0].run(task)
207
+ output = self._format_result(result)
208
+ else:
209
+ # Multi-agent → conversation
210
+ conv_result = self._conversation.run(
211
+ topic=task,
212
+ rounds=2,
213
+ initial_context=f"Team purpose: {self.purpose}",
214
+ )
215
+ output = conv_result.summary or str(conv_result.data.get("final_summary", ""))
216
+
217
+ self._history.append({"task": task, "result": output[:500]})
218
+
219
+ if verbose:
220
+ print(f"\n✅ Done!")
221
+ print(f" Tasks completed: {len(self._history)}")
222
+ print(f" (The team learns from each task — it gets better over time)")
223
+
224
+ return output
225
+
226
+ def ask(self, question: str) -> str:
227
+ """Shorthand for run() — more natural for Q&A use cases."""
228
+ return self.run(question, verbose=False)
229
+
230
+ def teach(self, lesson: str) -> None:
231
+ """
232
+ Teach the team something. This goes directly into their memory.
233
+
234
+ Example:
235
+ team.teach("Always cite your sources")
236
+ team.teach("When writing code, add docstrings to every function")
237
+ """
238
+ for agent in self._agents:
239
+ from purpose_agent.types import Heuristic, MemoryTier
240
+ h = Heuristic(
241
+ pattern="Always",
242
+ strategy=lesson,
243
+ steps=[],
244
+ tier=MemoryTier.STRATEGIC,
245
+ q_value=1.0,
246
+ )
247
+ agent.orch.optimizer.heuristic_library.append(h)
248
+ agent.orch.sync_memory()
249
+ print(f"📝 Taught all {len(self._agents)} agents: \"{lesson}\"")
250
+
251
+ def status(self) -> str:
252
+ """Show what the team has learned."""
253
+ lines = [f"🧠 Team Status: {self.purpose}", ""]
254
+
255
+ # Agents
256
+ for agent in self._agents:
257
+ n_heuristics = len(agent.orch.optimizer.heuristic_library)
258
+ n_experiences = agent.orch.experience_replay.size
259
+ lines.append(f" 🤖 {agent.name}: {n_heuristics} lessons learned, {n_experiences} experiences")
260
+
261
+ # History
262
+ lines.append(f"\n 📋 Tasks completed: {len(self._history)}")
263
+ for i, h in enumerate(self._history[-5:], 1):
264
+ lines.append(f" {i}. {h['task'][:60]}")
265
+
266
+ return "\n".join(lines)
267
+
268
+ @staticmethod
269
+ def _format_result(result: TaskResult) -> str:
270
+ """Convert a TaskResult into a readable string."""
271
+ data = result.final_state.data
272
+ # Try to get the most useful output
273
+ for key in ["_last_result", "_result", "result", "output", "answer"]:
274
+ if key in data and data[key]:
275
+ return str(data[key])
276
+ if result.final_state.summary:
277
+ return result.final_state.summary
278
+ return str(data)
279
+
280
+ @classmethod
281
+ def build(
282
+ cls,
283
+ purpose: str,
284
+ agents: list[str] | list[dict],
285
+ model: str | LLMBackend | None = None,
286
+ tools: list[Tool] | None = None,
287
+ ) -> "Team":
288
+ """
289
+ Build a custom team with named agents.
290
+
291
+ Args:
292
+ purpose: What the team does.
293
+ agents: List of agent names or {"name": ..., "role": ...} dicts.
294
+ model: AI model to use.
295
+ tools: Tools available to all agents.
296
+
297
+ Example:
298
+ team = Team.build(
299
+ purpose="Content creation",
300
+ agents=["writer", "editor", "fact_checker"],
301
+ model="qwen3:1.7b",
302
+ )
303
+ """
304
+ agent_specs = []
305
+ for a in agents:
306
+ if isinstance(a, str):
307
+ agent_specs.append({"name": a, "role": f"You are the {a}."})
308
+ else:
309
+ agent_specs.append(a)
310
+ return cls(purpose=purpose, agents=agent_specs, model=model, tools=tools)
311
+
312
+
313
+ # ═══════════════════════════════════════════════════════════════════════════
314
+ # Auto-Detection & Templates
315
+ # ═══════════════════════════════════════════════════════════════════════════
316
+
317
+ # Pre-built team templates matched by keywords in the purpose description
318
+ TEAM_TEMPLATES = {
319
+ "research": {
320
+ "name": "Research Team",
321
+ "keywords": ["research", "find", "search", "discover", "learn", "papers", "study", "investigate", "summarize", "analyze information"],
322
+ "agents": [
323
+ {"name": "researcher", "role": "Find and gather relevant information. Be thorough and cite sources."},
324
+ {"name": "analyst", "role": "Analyze the gathered information. Identify patterns, draw conclusions, and summarize findings clearly."},
325
+ ],
326
+ "tools": [CalculatorTool],
327
+ },
328
+ "coding": {
329
+ "name": "Coding Team",
330
+ "keywords": ["code", "program", "develop", "build", "software", "python", "javascript", "debug", "fix bug", "function", "api", "script"],
331
+ "agents": [
332
+ {"name": "architect", "role": "Design the solution. Break the problem into clear steps before coding."},
333
+ {"name": "coder", "role": "Write clean, well-documented code. Include error handling and comments."},
334
+ {"name": "tester", "role": "Review the code for bugs, edge cases, and improvements. Suggest fixes."},
335
+ ],
336
+ "tools": [PythonExecTool, CalculatorTool],
337
+ },
338
+ "writing": {
339
+ "name": "Writing Team",
340
+ "keywords": ["write", "blog", "article", "essay", "content", "copy", "draft", "edit", "proofread", "report", "documentation"],
341
+ "agents": [
342
+ {"name": "writer", "role": "Write clear, engaging content. Focus on the reader's needs."},
343
+ {"name": "editor", "role": "Review and improve the writing. Fix grammar, clarity, and flow. Be constructive."},
344
+ ],
345
+ "tools": [],
346
+ },
347
+ "data": {
348
+ "name": "Data Team",
349
+ "keywords": ["data", "csv", "excel", "spreadsheet", "database", "sql", "chart", "graph", "statistics", "analytics", "dashboard"],
350
+ "agents": [
351
+ {"name": "analyst", "role": "Analyze data, find patterns, and compute statistics."},
352
+ {"name": "reporter", "role": "Present findings in clear, non-technical language with key takeaways."},
353
+ ],
354
+ "tools": [PythonExecTool, CalculatorTool, ReadFileTool],
355
+ },
356
+ "assistant": {
357
+ "name": "General Assistant",
358
+ "keywords": ["help", "assist", "answer", "question", "explain", "general", "task", "do"],
359
+ "agents": [
360
+ {"name": "assistant", "role": "Help the user with their request. Be helpful, clear, and thorough."},
361
+ ],
362
+ "tools": [CalculatorTool],
363
+ },
364
+ }
365
+
366
+
367
+ def _analyze_purpose(description: str) -> dict:
368
+ """Match a purpose description to the best team template."""
369
+ desc_lower = description.lower()
370
+ best_template = None
371
+ best_score = 0
372
+
373
+ for template_key, template in TEAM_TEMPLATES.items():
374
+ score = 0
375
+ for keyword in template["keywords"]:
376
+ if keyword in desc_lower:
377
+ score += 1
378
+ # Bonus for exact match
379
+ if f" {keyword} " in f" {desc_lower} ":
380
+ score += 0.5
381
+ if score > best_score:
382
+ best_score = score
383
+ best_template = template
384
+
385
+ # Default to general assistant
386
+ if best_template is None or best_score < 0.5:
387
+ best_template = TEAM_TEMPLATES["assistant"]
388
+
389
+ return best_template
390
+
391
+
392
+ def _auto_detect_model(model: str | LLMBackend | None, prefer_local: bool) -> str | LLMBackend:
393
+ """Auto-detect the best available model."""
394
+ if model is not None:
395
+ return model
396
+
397
+ # Check for Ollama
398
+ if prefer_local:
399
+ try:
400
+ import urllib.request
401
+ urllib.request.urlopen("http://localhost:11434/api/tags", timeout=2)
402
+ logger.info("🟢 Ollama detected — using local models (free, private)")
403
+ return "qwen3:1.7b"
404
+ except Exception:
405
+ pass
406
+
407
+ # Check for API keys
408
+ if os.environ.get("OPENAI_API_KEY"):
409
+ logger.info("🔑 OpenAI API key found — using gpt-4o-mini")
410
+ return "gpt-4o-mini"
411
+
412
+ # Fallback: mock for testing
413
+ logger.info(
414
+ "💡 No local model detected. Using mock backend for testing.\n"
415
+ " To use a real model:\n"
416
+ " • Install Ollama: https://ollama.ai (free, local, private)\n"
417
+ " • Or set OPENAI_API_KEY for OpenAI\n"
418
+ " • Or set HF_TOKEN for HuggingFace"
419
+ )
420
+ return MockLLMBackend()
421
+
422
+
423
+ def _build_knowledge_store(knowledge: list[str] | str) -> KnowledgeStore:
424
+ """Build a KnowledgeStore from various input types."""
425
+ if isinstance(knowledge, list):
426
+ return KnowledgeStore.from_texts(knowledge)
427
+ elif os.path.isdir(knowledge):
428
+ return KnowledgeStore.from_directory(knowledge, glob="*.*")
429
+ elif os.path.isfile(knowledge):
430
+ kb = KnowledgeStore()
431
+ kb.add_file(knowledge)
432
+ return kb
433
+ else:
434
+ # Treat as a single text
435
+ return KnowledgeStore.from_texts([knowledge])
436
+
437
+
438
+ # ═══════════════════════════════════════════════════════════════════════════
439
+ # CLI Quickstart Wizard
440
+ # ═══════════════════════════════════════════════════════════════════════════
441
+
442
+ def quickstart():
443
+ """
444
+ Interactive wizard for creating an agent team. Run from command line:
445
+
446
+ python -m purpose_agent
447
+
448
+ Walks the user through setup step by step.
449
+ """
450
+ print()
451
+ print("╔══════════════════════════════════════════════════════════╗")
452
+ print("║ 🧠 Purpose Agent — Quickstart Wizard ║")
453
+ print("║ Build a self-improving AI team in under 60 seconds. ║")
454
+ print("╚══════════════════════════════════════════════════════════╝")
455
+ print()
456
+
457
+ # Step 1: What's your purpose?
458
+ print("Step 1: What do you want your AI team to do?")
459
+ print(" Examples: 'research assistant', 'code helper', 'content writer'")
460
+ print()
461
+ user_purpose = input(" Your purpose: ").strip()
462
+ if not user_purpose:
463
+ user_purpose = "general assistant"
464
+ print()
465
+
466
+ # Step 2: Model selection
467
+ print("Step 2: Which AI model? (press Enter for auto-detect)")
468
+ print(" • Enter → auto-detect (recommended)")
469
+ print(" • 'local' → use Ollama (free, private)")
470
+ print(" • 'cloud' → use HuggingFace cloud")
471
+ print(" • 'openai' → use OpenAI")
472
+ print()
473
+ model_choice = input(" Model: ").strip().lower()
474
+
475
+ if model_choice == "local":
476
+ model = "qwen3:1.7b"
477
+ elif model_choice == "cloud":
478
+ model = "Qwen/Qwen3-32B"
479
+ elif model_choice == "openai":
480
+ model = "gpt-4o-mini"
481
+ elif model_choice:
482
+ model = model_choice
483
+ else:
484
+ model = None # auto-detect
485
+ print()
486
+
487
+ # Step 3: Knowledge?
488
+ print("Step 3: Do you have documents for your team to learn from?")
489
+ print(" • Enter → no documents")
490
+ print(" • folder path → load all files from folder")
491
+ print(" • file path → load a specific file")
492
+ print()
493
+ knowledge_input = input(" Documents: ").strip()
494
+ knowledge = knowledge_input if knowledge_input else None
495
+ print()
496
+
497
+ # Build!
498
+ print("━" * 50)
499
+ print("Building your team...")
500
+ print()
501
+
502
+ team = purpose(user_purpose, model=model, knowledge=knowledge)
503
+
504
+ print()
505
+ print("✅ Your team is ready!")
506
+ print()
507
+ print("Try it now — type a task (or 'quit' to exit):")
508
+ print()
509
+
510
+ while True:
511
+ try:
512
+ task = input("📝 Task: ").strip()
513
+ except (EOFError, KeyboardInterrupt):
514
+ print("\n👋 Goodbye!")
515
+ break
516
+
517
+ if not task or task.lower() in ("quit", "exit", "q"):
518
+ print("\n👋 Goodbye!")
519
+ break
520
+
521
+ if task.lower() == "status":
522
+ print(team.status())
523
+ continue
524
+
525
+ if task.lower().startswith("teach:"):
526
+ lesson = task[6:].strip()
527
+ team.teach(lesson)
528
+ continue
529
+
530
+ result = team.run(task)
531
+ print(f"\n💡 Result:\n{result}\n")