Skip to content

Commit 4504826

Browse files
committed
feat: add pip-installable agent-patterns package with core Agent class and tool system
1 parent d6f711f commit 4504826

4 files changed

Lines changed: 407 additions & 20 deletions

File tree

pyproject.toml

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,48 @@
11
[build-system]
22
requires = ["setuptools>=68.0", "wheel"]
3-
build-backend = "setuptools.backends._legacy:_Backend"
3+
build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "agent-patterns"
77
version = "0.1.0"
8-
description = "Battle-tested patterns for building self-evolving AI agents — from a production system with 1,000+ autonomous cycles. https://github.com/tutuoai/agent-patterns"
8+
description = "Battle-tested patterns for building self-evolving AI agents — from a production system with 1,000+ autonomous cycles."
99
readme = "README.md"
1010
license = {text = "MIT"}
1111
requires-python = ">=3.11"
1212
authors = [
1313
{name = "TutuoAI", email = "hello@tutuoai.com"},
1414
]
15-
keywords = ["ai", "agent", "self-evolving", "claude", "llm", "patterns"]
15+
keywords = [
16+
"ai", "agent", "self-evolving", "llm", "claude", "autonomous",
17+
"tool-use", "context-engineering", "ai-agent", "self-improving",
18+
]
1619
classifiers = [
1720
"Development Status :: 4 - Beta",
1821
"Intended Audience :: Developers",
1922
"License :: OSI Approved :: MIT License",
23+
"Programming Language :: Python :: 3",
2024
"Programming Language :: Python :: 3.11",
2125
"Programming Language :: Python :: 3.12",
2226
"Programming Language :: Python :: 3.13",
2327
"Topic :: Scientific/Engineering :: Artificial Intelligence",
28+
"Topic :: Software Development :: Libraries :: Python Modules",
2429
]
2530
dependencies = [
2631
"anthropic>=0.39.0",
27-
"pyyaml>=6.0",
28-
]
29-
30-
[project.optional-dependencies]
31-
dev = [
32-
"pytest>=8.0",
33-
"ruff>=0.5.0",
3432
]
3533

3634
[project.urls]
37-
Homepage = "https://github.com/tutuoai/agent-patterns"
38-
Documentation = "https://tutuoai.com"
35+
Homepage = "https://github.com/AFunLS/self-evolving-agent-patterns"
36+
Documentation = "https://github.com/AFunLS/self-evolving-agent-patterns#readme"
37+
Repository = "https://github.com/AFunLS/self-evolving-agent-patterns"
3938
"Premium Guides" = "https://tutuoai.com"
40-
"Bug Tracker" = "https://github.com/tutuoai/agent-patterns/issues"
39+
"Bug Tracker" = "https://github.com/AFunLS/self-evolving-agent-patterns/issues"
4140

42-
[tool.ruff]
43-
target-version = "py311"
44-
line-length = 100
41+
[project.scripts]
42+
agent-patterns = "agent_patterns.cli:main"
4543

46-
[tool.ruff.lint]
47-
select = ["E", "F", "W", "I"]
44+
[tool.setuptools.packages.find]
45+
where = ["src"]
4846

49-
[tool.pytest.ini_options]
50-
testpaths = ["tests"]
47+
[tool.setuptools.package-data]
48+
agent_patterns = ["*.md", "*.yaml"]

src/agent_patterns/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""
2+
agent-patterns — Battle-tested patterns for building self-evolving AI agents.
3+
4+
Extracted from a production system with 1,000+ autonomous cycles.
5+
https://github.com/AFunLS/self-evolving-agent-patterns
6+
7+
Quick start:
8+
from agent_patterns import Agent, tool
9+
10+
@tool("greet", "Greet someone by name")
11+
def greet(name: str) -> str:
12+
return f"Hello, {name}!"
13+
14+
agent = Agent(model="claude-sonnet-4-20250514")
15+
agent.add_tool(greet)
16+
agent.run("Greet the user warmly")
17+
18+
For premium guides on context engineering, self-evolution, multi-agent
19+
orchestration, and more: https://tutuoai.com
20+
"""
21+
22+
__version__ = "0.1.0"
23+
24+
from agent_patterns.core import Agent
25+
from agent_patterns.tools import tool
26+
27+
__all__ = ["Agent", "tool", "__version__"]

src/agent_patterns/core.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
"""
2+
Core Agent class — a minimal but production-pattern AI agent.
3+
4+
This is the main entry point for agent-patterns. It implements the
5+
context-engineering pattern: control LLM behavior through INPUT, not output parsing.
6+
7+
Usage:
8+
from agent_patterns import Agent
9+
10+
agent = Agent(model="claude-sonnet-4-20250514")
11+
result = agent.run("Create a hello world script and verify it works")
12+
13+
For self-evolution (the agent modifies its own behavior):
14+
agent = Agent(model="claude-sonnet-4-20250514", self_evolving=True)
15+
16+
Full production guide: https://tutuoai.com
17+
"""
18+
19+
from __future__ import annotations
20+
21+
import json
22+
from datetime import datetime
23+
from pathlib import Path
24+
from typing import Any
25+
26+
import anthropic
27+
28+
from agent_patterns.tools import BUILTIN_TOOLS, ToolDef
29+
30+
31+
class Agent:
32+
"""
33+
A minimal, production-pattern AI agent with tool use and optional self-evolution.
34+
35+
Key patterns implemented:
36+
- Context engineering: system prompt rebuilt every turn
37+
- Tool use: real tools with automatic dispatch
38+
- Self-evolution: agent can modify its own prompt (opt-in)
39+
- Learning: outcomes logged for future context injection
40+
"""
41+
42+
def __init__(
43+
self,
44+
*,
45+
model: str = "claude-sonnet-4-20250514",
46+
system_prompt: str | None = None,
47+
max_turns: int = 10,
48+
self_evolving: bool = False,
49+
prompt_file: str | Path = "agent_prompt.md",
50+
reward_log: str | Path = "rewards.jsonl",
51+
max_output: int = 10000,
52+
):
53+
self.model = model
54+
self._base_prompt = system_prompt or self._default_prompt()
55+
self.max_turns = max_turns
56+
self.self_evolving = self_evolving
57+
self.prompt_file = Path(prompt_file)
58+
self.reward_log = Path(reward_log)
59+
self.max_output = max_output
60+
self._tools: dict[str, ToolDef] = {}
61+
self._client = anthropic.Anthropic()
62+
63+
# Register built-in tools
64+
for t in BUILTIN_TOOLS:
65+
self._tools[t.name] = t
66+
67+
# Add self-evolution tools if enabled
68+
if self_evolving:
69+
self._add_evolution_tools()
70+
71+
def add_tool(self, tool_def: ToolDef) -> "Agent":
72+
"""Register a tool. Chainable: agent.add_tool(t1).add_tool(t2)"""
73+
self._tools[tool_def.name] = tool_def
74+
return self
75+
76+
def run(self, task: str, *, verbose: bool = True) -> dict[str, Any]:
77+
"""
78+
Run one agent cycle: given a task, use tools to complete it.
79+
80+
Returns a dict with keys: turns, messages, final_text
81+
"""
82+
messages: list[dict] = [{"role": "user", "content": task}]
83+
final_text = ""
84+
85+
if verbose:
86+
print(f"\n{'='*60}")
87+
print(f"🧬 Agent Cycle — Task: {task[:80]}")
88+
print(f"{'='*60}\n")
89+
90+
for turn in range(self.max_turns):
91+
response = self._client.messages.create(
92+
model=self.model,
93+
max_tokens=4096,
94+
system=self._build_system_prompt(),
95+
tools=[t.to_api_schema() for t in self._tools.values()],
96+
messages=messages,
97+
)
98+
99+
tool_used = False
100+
for block in response.content:
101+
if block.type == "text" and block.text.strip():
102+
final_text = block.text
103+
if verbose:
104+
print(f"💭 {block.text[:300]}")
105+
106+
elif block.type == "tool_use":
107+
tool_used = True
108+
tool_def = self._tools.get(block.name)
109+
if verbose:
110+
print(f"🔧 {block.name}({json.dumps(block.input)[:100]})")
111+
112+
if tool_def:
113+
result = tool_def.call(**block.input)
114+
else:
115+
result = f"Error: unknown tool '{block.name}'"
116+
117+
result = result[:self.max_output]
118+
if verbose:
119+
print(f" → {result[:200]}")
120+
121+
messages.append({"role": "assistant", "content": response.content})
122+
messages.append({
123+
"role": "user",
124+
"content": [
125+
{"type": "tool_result", "tool_use_id": block.id, "content": result}
126+
],
127+
})
128+
129+
if response.stop_reason == "end_turn" and not tool_used:
130+
if verbose:
131+
print(f"\n✅ Cycle complete (turn {turn + 1}/{self.max_turns})")
132+
break
133+
134+
return {"turns": turn + 1, "messages": messages, "final_text": final_text}
135+
136+
def _build_system_prompt(self) -> str:
137+
"""
138+
Rebuild the system prompt every turn.
139+
140+
This is THE key pattern: context engineering. The agent sees
141+
its base identity + accumulated knowledge + recent history.
142+
"""
143+
prompt = self._base_prompt
144+
145+
# Layer on self-modified knowledge
146+
if self.self_evolving and self.prompt_file.exists():
147+
knowledge = self.prompt_file.read_text().strip()
148+
if knowledge:
149+
prompt += f"\n\n## Your Accumulated Knowledge\n{knowledge}\n"
150+
151+
# Layer on recent reward history
152+
if self.reward_log.exists():
153+
try:
154+
lines = self.reward_log.read_text().strip().split("\n")
155+
recent = lines[-5:]
156+
prompt += "\n## Recent History (learn from this)\n"
157+
for line in recent:
158+
entry = json.loads(line)
159+
prompt += f"- [{entry.get('judgment', '?')}] {entry.get('lesson', '?')}\n"
160+
except Exception:
161+
pass # Non-critical — don't crash on corrupt history
162+
163+
return prompt
164+
165+
def _add_evolution_tools(self) -> None:
166+
"""Add self-evolution tools: modify_prompt and record_result."""
167+
168+
def modify_prompt(addition: str) -> str:
169+
current = self.prompt_file.read_text() if self.prompt_file.exists() else ""
170+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
171+
self.prompt_file.write_text(
172+
current + f"\n\n## Learned ({timestamp})\n{addition}\n"
173+
)
174+
return "✅ System prompt updated — this will affect all future cycles."
175+
176+
def record_result(judgment: str, lesson: str) -> str:
177+
entry = {"judgment": judgment, "lesson": lesson, "timestamp": datetime.now().isoformat()}
178+
with open(self.reward_log, "a") as f:
179+
f.write(json.dumps(entry) + "\n")
180+
return f"Recorded: {judgment}{lesson}"
181+
182+
self._tools["modify_prompt"] = ToolDef(
183+
name="modify_prompt",
184+
description=(
185+
"Append a new rule or insight to the agent's own system prompt. "
186+
"This permanently changes future behavior — the core of self-evolution."
187+
),
188+
handler=modify_prompt,
189+
parameters={"addition": {"type": "string", "description": "Text to append"}},
190+
)
191+
192+
self._tools["record_result"] = ToolDef(
193+
name="record_result",
194+
description="Record the outcome of this cycle for future learning.",
195+
handler=record_result,
196+
parameters={
197+
"judgment": {
198+
"type": "string",
199+
"enum": ["success", "failure", "partial"],
200+
"description": "How did this cycle go?",
201+
},
202+
"lesson": {"type": "string", "description": "What to remember"},
203+
},
204+
)
205+
206+
@staticmethod
207+
def _default_prompt() -> str:
208+
return (
209+
"You are a self-evolving AI agent. You execute tasks using tools, learn from "
210+
"outcomes, and improve your own behavior by modifying your system prompt.\n\n"
211+
"## Rules\n"
212+
"- Use tools to act, don't just talk about acting\n"
213+
"- After completing a task, call record_result with your judgment and lesson\n"
214+
"- If you discover a reusable insight, call modify_prompt to remember it\n"
215+
"- Be concrete: produce files, run commands, verify results\n"
216+
)

0 commit comments

Comments
 (0)