agent harness initialize repo
This commit is contained in:
191
agent-planning/agent.py
Normal file
191
agent-planning/agent.py
Normal file
@@ -0,0 +1,191 @@
|
||||
import os
|
||||
import json
|
||||
from openai import OpenAI
|
||||
from tools import get_tool_registry, get_tool_schemas
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
TOOL_REGISTRY = get_tool_registry()
|
||||
TOOL_SCHEMAS = get_tool_schemas()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent loop
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_llm_client():
|
||||
return OpenAI(
|
||||
base_url="http://localhost:11434/v1",
|
||||
api_key="."
|
||||
)
|
||||
|
||||
|
||||
def handle_tool_calls(tool_calls, messages):
|
||||
"""Execute each tool the LLM requested and append the results to messages."""
|
||||
for tool_call in tool_calls:
|
||||
name = tool_call.function.name
|
||||
args = json.loads(tool_call.function.arguments)
|
||||
|
||||
print(f" [tool] {name}({args})")
|
||||
|
||||
if name not in TOOL_REGISTRY:
|
||||
result = f"Error: unknown tool '{
|
||||
name}'. Available tools: {list(TOOL_REGISTRY.keys())}"
|
||||
else:
|
||||
try:
|
||||
result = TOOL_REGISTRY[name](**args)
|
||||
except TypeError as e:
|
||||
result = (
|
||||
f"Error: invalid arguments for tool '{name}': {e}. "
|
||||
"Check the tool schema and retry with the correct arguments."
|
||||
)
|
||||
|
||||
print(f" [tool result] {result[:200]}{
|
||||
'...' if len(result) > 200 else ''}")
|
||||
|
||||
# The LLM needs the result tied back to the specific tool call id
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_call.id,
|
||||
"content": result,
|
||||
})
|
||||
|
||||
|
||||
def agent_loop(client):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"You are a capable coding and research assistant.\n\n"
|
||||
|
||||
"## Available tools\n\n"
|
||||
"Action tools: read_file, write_file, edit_file, glob_files, grep, run_bash, webfetch\n\n"
|
||||
"Planning tools:\n"
|
||||
"- Scratchpad (read_scratchpad / write_scratchpad): your private working memory. "
|
||||
"Use it to think through an approach, store intermediate findings, or draft content "
|
||||
"before committing. Each write fully replaces the previous content.\n"
|
||||
"- To-do list (todo_append / todo_list / todo_update): a persistent task tracker. "
|
||||
"Items carry a status: pending, in_progress, done, cancelled, or failed.\n\n"
|
||||
|
||||
"## Working directory\n\n"
|
||||
"The current working directory is always the user's project root. "
|
||||
"When asked to work on a project or codebase without a specified path, "
|
||||
"start by exploring '.' with glob_files or run_bash. "
|
||||
"Never ask the user to supply a path.\n\n"
|
||||
|
||||
"## How to plan\n\n"
|
||||
"For complex or multi-step tasks (roughly 3 or more distinct steps, or when the "
|
||||
"path forward is unclear):\n"
|
||||
"1. Write your initial thinking and approach to the scratchpad before acting.\n"
|
||||
"2. Break the work into concrete steps and add each one to the to-do list with "
|
||||
"todo_append (status: pending).\n"
|
||||
"3. Before starting a step, mark it in_progress with todo_update. "
|
||||
"Keep only one item in_progress at a time.\n"
|
||||
"4. Mark items done immediately after completing them — do not batch completions.\n"
|
||||
"5. Call todo_list to review remaining work before moving to the next step.\n"
|
||||
"6. Mark tasks cancelled if they become unnecessary.\n\n"
|
||||
"For simple, single-step tasks: act directly without creating todos.\n\n"
|
||||
"Planning tool calls (write_scratchpad, todo_append, todo_update, todo_list) "
|
||||
"are internal bookkeeping, not responses to the user. After any planning tool "
|
||||
"call, always continue working immediately — make your next tool call or, once "
|
||||
"the task is fully complete, give a substantive final answer. "
|
||||
"Never emit an empty or whitespace-only message.\n\n"
|
||||
"## Replanning\n\n"
|
||||
"After every tool result, check whether the outcome matched your expectation. "
|
||||
"If a tool returns an error, unexpected output, or reveals information that "
|
||||
"changes your understanding of the task, do not move to the next planned step — "
|
||||
"replan first.\n\n"
|
||||
"When a step fails:\n"
|
||||
"1. Diagnose in the scratchpad — is this a recoverable input error (wrong path, "
|
||||
"typo, wrong argument) or a deeper problem (wrong approach, wrong assumption)?\n"
|
||||
"2. Mark the task failed: todo_update(id, status='failed').\n"
|
||||
"3. Choose a recovery action:\n"
|
||||
" - Retry: the failure is correctable. Fix the input and set the task back to "
|
||||
"in_progress. The tool will report which retry attempt this is.\n"
|
||||
" - Replace: the approach is wrong. Cancel the task and add a revised one.\n"
|
||||
" - Reorder: new information makes a different task more urgent. Update the "
|
||||
"pending items before continuing.\n"
|
||||
"4. If todo_update reports that the retry limit has been reached, stop retrying. "
|
||||
"Write a clear diagnosis in the scratchpad — what you tried, what failed each "
|
||||
"time, and what you need — then give the user a concise escalation message "
|
||||
"and wait for their input.\n\n"
|
||||
"When a tool succeeds but returns information that changes the picture, pause "
|
||||
"before acting. Call todo_list, reassess all pending items in the scratchpad, "
|
||||
"and cancel or replace any tasks that no longer make sense.\n\n"
|
||||
"## How to use the scratchpad\n\n"
|
||||
"Before each tool call during a complex task, update the scratchpad with your "
|
||||
"current thinking. Structure each entry around these five steps:\n\n"
|
||||
"1. Restate the goal — write what you understand the task to be, in your own words. "
|
||||
"This catches misreads before they compound into wasted work.\n"
|
||||
"2. Survey what you know — note which files you have seen, what the code structure "
|
||||
"looks like, and what constraints or requirements apply.\n"
|
||||
"3. Evaluate options — reason through at least two approaches and explain why you "
|
||||
"are choosing one over the other (e.g. 'I could rewrite the middleware, or wrap it. "
|
||||
"Wrapping is safer because it leaves the existing call sites untouched.').\n"
|
||||
"4. Anticipate failure modes — write down what could go wrong with the chosen "
|
||||
"approach and how you would diagnose it (e.g. 'If the tests fail after this, the "
|
||||
"most likely cause is that the session cookie name changed.').\n"
|
||||
"5. Decide the next single action — commit to exactly one tool call. "
|
||||
"Do not plan several calls at once; decide the next step only.\n\n"
|
||||
"Re-read the scratchpad whenever you resume after a tool result to keep your "
|
||||
"reasoning grounded in what you have already learned.\n\n"
|
||||
"## Done detection\n\n"
|
||||
"Do not give a final answer based on the task list being empty alone. "
|
||||
"Before declaring the task complete, verify all three of the following:\n\n"
|
||||
"1. Structural completion — call todo_list and confirm there are no pending, "
|
||||
"in_progress, or failed items.\n"
|
||||
"2. Verification — check the output against the original goal. For code tasks: "
|
||||
"run the tests or build with run_bash and confirm they pass. For research tasks: "
|
||||
"re-read the scratchpad and confirm the assembled answer addresses what was "
|
||||
"actually asked.\n"
|
||||
"3. Uncertainty check — read the scratchpad and ask: are there unresolved "
|
||||
"questions, assumptions that were never validated, or tasks that were cancelled "
|
||||
"rather than properly completed?\n\n"
|
||||
"If all three are satisfied, give your final answer. If any are not, re-enter "
|
||||
"the planning loop — add the outstanding items to the todo list and continue."
|
||||
),
|
||||
}
|
||||
]
|
||||
|
||||
while True:
|
||||
user_input = input("You: ")
|
||||
if user_input.lower() == "\\exit":
|
||||
break
|
||||
|
||||
messages.append({"role": "user", "content": user_input})
|
||||
|
||||
# Keep looping until the LLM stops calling tools and gives a final reply
|
||||
while True:
|
||||
response = client.chat.completions.create(
|
||||
model="gemma4",
|
||||
messages=messages,
|
||||
tools=TOOL_SCHEMAS,
|
||||
temperature=0.7,
|
||||
)
|
||||
|
||||
message = response.choices[0].message
|
||||
|
||||
# Always append the assistant turn so the conversation stays intact
|
||||
messages.append(message)
|
||||
|
||||
if message.tool_calls:
|
||||
# The LLM wants to use one or more tools — run them, then loop
|
||||
handle_tool_calls(message.tool_calls, messages)
|
||||
elif not message.content or not message.content.strip():
|
||||
# The model ended its turn with an empty message — most commonly
|
||||
# happens after a planning-only tool call (scratchpad / todo).
|
||||
# Nudge it to continue rather than silently stalling.
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": "Continue.",
|
||||
})
|
||||
else:
|
||||
# No tool calls: we have the final answer
|
||||
print(f"Assistant: {message.content}")
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
client = get_llm_client()
|
||||
agent_loop(client)
|
||||
3
agent-planning/tools/__init__.py
Normal file
3
agent-planning/tools/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from tools.registry import get_tool_registry, get_tool_schemas
|
||||
|
||||
__all__ = ["get_tool_registry", "get_tool_schemas"]
|
||||
61
agent-planning/tools/filesystem.py
Normal file
61
agent-planning/tools/filesystem.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import glob as glob_module
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def read_file(path: str, offset: int = 1, limit: int = 200) -> str:
|
||||
"""Read lines from a file, with optional offset and limit."""
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
return f"Error: file not found: {path}"
|
||||
elif p.is_dir():
|
||||
return f"Error: cannot read directory content using read_file for path: {path}"
|
||||
else:
|
||||
# Proceed with file reading logic
|
||||
lines = p.read_text(errors="replace").splitlines()
|
||||
selected = lines[offset - 1: offset - 1 + limit]
|
||||
return "\n".join(f"{offset + i}: {line}" for i, line in enumerate(selected))
|
||||
|
||||
|
||||
def glob_files(pattern: str, path: str = ".") -> str:
|
||||
"""Find files matching a glob pattern inside a directory."""
|
||||
matches = glob_module.glob(f"{path}/**/{pattern}", recursive=True)
|
||||
matches += glob_module.glob(f"{path}/{pattern}")
|
||||
unique = sorted(set(matches))
|
||||
return "\n".join(unique) if unique else "(no matches)"
|
||||
|
||||
|
||||
def grep(pattern: str, path: str = ".", include: str = "*") -> str:
|
||||
"""Search file contents for a regex pattern, optionally filtering by filename glob."""
|
||||
results = []
|
||||
for filepath in glob_module.glob(f"{path}/**/{include}", recursive=True):
|
||||
fp = Path(filepath)
|
||||
if not fp.is_file():
|
||||
continue
|
||||
try:
|
||||
for i, line in enumerate(fp.read_text(errors="replace").splitlines(), 1):
|
||||
if re.search(pattern, line):
|
||||
results.append(f"{filepath}:{i}: {line}")
|
||||
except OSError:
|
||||
pass
|
||||
return "\n".join(results) if results else "(no matches)"
|
||||
|
||||
|
||||
def write_file(path: str, content: str) -> str:
|
||||
"""Write content to a file, creating it if it does not exist."""
|
||||
p = Path(path)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(content)
|
||||
return f"Wrote {len(content)} bytes to {path}"
|
||||
|
||||
|
||||
def edit_file(path: str, old_string: str, new_string: str) -> str:
|
||||
"""Replace the first occurrence of old_string with new_string in a file."""
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
return f"Error: file not found: {path}"
|
||||
original = p.read_text()
|
||||
if old_string not in original:
|
||||
return f"Error: string not found in {path}"
|
||||
p.write_text(original.replace(old_string, new_string, 1))
|
||||
return f"Edited {path}"
|
||||
256
agent-planning/tools/registry.py
Normal file
256
agent-planning/tools/registry.py
Normal file
@@ -0,0 +1,256 @@
|
||||
from tools.filesystem import read_file, glob_files, grep, write_file, edit_file
|
||||
from tools.shell import run_bash
|
||||
from tools.web import webfetch
|
||||
from tools.todo import todo_append, todo_list, todo_update
|
||||
from tools.scratchpad import read_scratchpad, write_scratchpad
|
||||
|
||||
|
||||
def get_tool_registry():
|
||||
return {
|
||||
"run_bash": run_bash,
|
||||
"read_file": read_file,
|
||||
"glob_files": glob_files,
|
||||
"grep": grep,
|
||||
"write_file": write_file,
|
||||
"edit_file": edit_file,
|
||||
"webfetch": webfetch,
|
||||
"todo_append": todo_append,
|
||||
"todo_list": todo_list,
|
||||
"todo_update": todo_update,
|
||||
"read_scratchpad": read_scratchpad,
|
||||
"write_scratchpad": write_scratchpad,
|
||||
}
|
||||
|
||||
|
||||
def get_tool_schemas():
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "run_bash",
|
||||
"description": "Run a bash command on the user's machine and return the output.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The bash command to execute.",
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Read lines from a file. Returns lines prefixed with line numbers.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Absolute or relative path to the file."},
|
||||
"offset": {"type": "integer", "description": "First line to read (1-indexed). Defaults to 1."},
|
||||
"limit": {"type": "integer", "description": "Maximum number of lines to return. Defaults to 200."},
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "glob_files",
|
||||
"description": "Find files matching a glob pattern (e.g. '**/*.py') inside a directory.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {"type": "string", "description": "Glob pattern to match against file names."},
|
||||
"path": {"type": "string", "description": "Root directory to search in. Defaults to '.'."},
|
||||
},
|
||||
"required": ["pattern"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "grep",
|
||||
"description": "Search file contents for a regex pattern and return matching lines with file paths and line numbers.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {"type": "string", "description": "Regular expression to search for."},
|
||||
"path": {"type": "string", "description": "Directory to search in. Defaults to '.'."},
|
||||
"include": {"type": "string", "description": "Filename glob to restrict which files are searched (e.g. '*.py'). Defaults to '*'."},
|
||||
},
|
||||
"required": ["pattern"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_file",
|
||||
"description": "Write content to a file, creating it (and any missing parent directories) if it does not exist.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Path of the file to write."},
|
||||
"content": {"type": "string", "description": "Full content to write to the file."},
|
||||
},
|
||||
"required": ["path", "content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "edit_file",
|
||||
"description": "Replace the first occurrence of a string in a file with a new string.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string", "description": "Path of the file to edit."},
|
||||
"old_string": {"type": "string", "description": "Exact string to find and replace."},
|
||||
"new_string": {"type": "string", "description": "String to replace it with."},
|
||||
},
|
||||
"required": ["path", "old_string", "new_string"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "webfetch",
|
||||
"description": (
|
||||
"Fetch a public URL (http/https only) and return its full plain-text content (up to 2 MB)."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {"type": "string", "description": "The URL to fetch (http/https)."},
|
||||
},
|
||||
"required": ["url"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "todo_append",
|
||||
"description": (
|
||||
"Add a new item to the to-do list. "
|
||||
"Use this to track a task you plan to work on."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier for the item (e.g. '1', 'task-setup').",
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Description of the task.",
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "in_progress", "done", "cancelled", "failed"],
|
||||
"description": "Initial status of the item. Use 'pending' for new tasks.",
|
||||
},
|
||||
},
|
||||
"required": ["id", "content", "status"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "todo_list",
|
||||
"description": (
|
||||
"Read the current to-do list. "
|
||||
"By default shows all active items (pending, in_progress, failed). "
|
||||
"Set include_completed=true to also see done and cancelled items. "
|
||||
"Failed items display their retry count."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"include_completed": {
|
||||
"type": "boolean",
|
||||
"description": "If true, include done and cancelled items in the output. Defaults to false.",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "todo_update",
|
||||
"description": (
|
||||
"Update the content or status of an existing to-do item. "
|
||||
"At least one of 'content' or 'status' must be provided. "
|
||||
"Setting a failed item back to in_progress counts as a retry and "
|
||||
"is tracked automatically. The response will warn when the retry "
|
||||
"limit is reached."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID of the to-do item to update.",
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "New description for the item. Omit to leave unchanged.",
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "in_progress", "done", "cancelled", "failed"],
|
||||
"description": "New status for the item. Omit to leave unchanged.",
|
||||
},
|
||||
},
|
||||
"required": ["id"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_scratchpad",
|
||||
"description": (
|
||||
"Read the current contents of the in-memory scratchpad. "
|
||||
"Returns '(empty)' if nothing has been written yet."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_scratchpad",
|
||||
"description": (
|
||||
"Overwrite the entire contents of the in-memory scratchpad with new content. "
|
||||
"The previous content is permanently replaced."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "The new content to store in the scratchpad.",
|
||||
},
|
||||
},
|
||||
"required": ["content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
31
agent-planning/tools/scratchpad.py
Normal file
31
agent-planning/tools/scratchpad.py
Normal file
@@ -0,0 +1,31 @@
|
||||
class Scratchpad:
|
||||
"""Read and write from a in-memory scratchpad"""
|
||||
|
||||
def __init__(self):
|
||||
self._content = ""
|
||||
|
||||
def read(self) -> str:
|
||||
if self._content == "":
|
||||
return "(empty)"
|
||||
return self._content
|
||||
|
||||
def write(self, content: str) -> str:
|
||||
self._content = str(content).strip()
|
||||
return self._content
|
||||
|
||||
|
||||
scratchpad = Scratchpad()
|
||||
|
||||
|
||||
def read_scratchpad():
|
||||
"""Read the contents of the scratchpad"""
|
||||
return scratchpad.read()
|
||||
|
||||
|
||||
def write_scratchpad(content: str):
|
||||
"""
|
||||
Write into the scratchpad. The previous content
|
||||
will be overwritten.
|
||||
"""
|
||||
scratchpad.write(content)
|
||||
return "Successfully written content into scratchpad"
|
||||
12
agent-planning/tools/shell.py
Normal file
12
agent-planning/tools/shell.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import subprocess
|
||||
|
||||
|
||||
def run_bash(command: str) -> str:
|
||||
"""Run a bash command and return its output."""
|
||||
result = subprocess.run(
|
||||
command, shell=True, text=True, capture_output=True
|
||||
)
|
||||
output = result.stdout
|
||||
if result.stderr:
|
||||
output += f"\nSTDERR:\n{result.stderr}"
|
||||
return output or "(no output)"
|
||||
116
agent-planning/tools/todo.py
Normal file
116
agent-planning/tools/todo.py
Normal file
@@ -0,0 +1,116 @@
|
||||
RETRY_LIMIT = 3
|
||||
|
||||
|
||||
class ToDoList:
|
||||
"""
|
||||
Helper class to hold a to-do list in memory
|
||||
"""
|
||||
|
||||
statuses = ["pending", "in_progress", "done", "cancelled", "failed"]
|
||||
|
||||
def __init__(self):
|
||||
self._items = []
|
||||
|
||||
def read(self, include_completed=False):
|
||||
"""Read the to-do list"""
|
||||
if include_completed:
|
||||
return [item.copy() for item in self._items]
|
||||
else:
|
||||
return [item.copy() for item in self._items
|
||||
if item["status"] != "done" and item["status"] != "cancelled"]
|
||||
|
||||
def append(self, id, content, status):
|
||||
if status not in ToDoList.statuses:
|
||||
raise Exception(f"Invalid status {status}. "
|
||||
"Valid to-do statuses: pending, in_progress, done, "
|
||||
"cancelled, failed")
|
||||
if self.contains(id):
|
||||
raise Exception(f"To do item {id} already exists!")
|
||||
new_item = {"id": id, "content": content,
|
||||
"status": status, "retries": 0}
|
||||
self._items.append(new_item)
|
||||
return new_item.copy()
|
||||
|
||||
def contains(self, id) -> bool:
|
||||
"""Check if the to do list contains an item with a specific id"""
|
||||
for item in self._items:
|
||||
if item["id"] == id:
|
||||
return True
|
||||
return False
|
||||
|
||||
def update(self, id, content, status):
|
||||
if status is not None and status not in ToDoList.statuses:
|
||||
raise Exception(f"Invalid status {status}. "
|
||||
"Valid to-do statuses: pending, in_progress, done, "
|
||||
"cancelled, failed")
|
||||
idx = 0
|
||||
while idx < len(self._items):
|
||||
if self._items[idx]["id"] == id:
|
||||
if content is not None:
|
||||
self._items[idx]["content"] = content
|
||||
if status is not None:
|
||||
prev_status = self._items[idx]["status"]
|
||||
self._items[idx]["status"] = status
|
||||
# A failed task being set back to in_progress is a retry attempt.
|
||||
if prev_status == "failed" and status == "in_progress":
|
||||
self._items[idx]["retries"] += 1
|
||||
return self._items[idx].copy()
|
||||
idx += 1
|
||||
raise Exception(f"To do item with id {id} not found")
|
||||
|
||||
|
||||
todo_store = ToDoList()
|
||||
|
||||
|
||||
def todo_append(id, content, status) -> str:
|
||||
"""Append a new to do item to the to do list"""
|
||||
id_str = str(id)
|
||||
content_str = str(content)
|
||||
status_str = str(status)
|
||||
try:
|
||||
todo_store.append(id_str, content_str, status_str)
|
||||
return f"Successfully appended to do item {id_str} in to do list!"
|
||||
except Exception as e:
|
||||
return f"Failed to append to do item: {e}"
|
||||
|
||||
|
||||
def todo_list(include_completed=False) -> str:
|
||||
"""List all the items in the to do list"""
|
||||
items = todo_store.read(include_completed)
|
||||
|
||||
result = f"To Do List ({len(items)} items)\n"
|
||||
for status in ToDoList.statuses:
|
||||
count = sum(1 for i in items if i["status"] == status)
|
||||
result += f"{count} {status} items\n"
|
||||
|
||||
result += "-----\n"
|
||||
for item in items:
|
||||
retry_note = f", {item['retries']
|
||||
} retries" if item["retries"] > 0 else ""
|
||||
result += f"- [{item['id']}] {item['content']
|
||||
} ({item['status']}{retry_note})\n"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def todo_update(id, content=None, status=None) -> str:
|
||||
if content is None and status is None:
|
||||
return "No content or status was given to update. Nothing to do."
|
||||
try:
|
||||
item = todo_store.update(id, content, status)
|
||||
retries = item["retries"]
|
||||
if item["status"] == "in_progress" and retries > 0:
|
||||
if retries >= RETRY_LIMIT:
|
||||
return (
|
||||
f"Updated to do item {id} to in_progress — "
|
||||
f"but this is retry {retries} of {
|
||||
RETRY_LIMIT} (retry limit reached). "
|
||||
f"Do not retry again. Escalate to the user instead."
|
||||
)
|
||||
return (
|
||||
f"Successfully updated to do item {id}! "
|
||||
f"Retry attempt {retries} of {RETRY_LIMIT}."
|
||||
)
|
||||
return f"Successfully updated to do item {id}!"
|
||||
except Exception as e:
|
||||
return f"Failed to update to do item {id}: {e}"
|
||||
41
agent-planning/tools/web.py
Normal file
41
agent-planning/tools/web.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import re
|
||||
import urllib.request
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
def webfetch(url: str) -> str:
|
||||
"""Fetch a URL and return its full plain-text content (up to 2 MB)."""
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
if parsed.scheme not in ("http", "https"):
|
||||
return f"Error fetching {url}: unsupported scheme '{parsed.scheme}'. Only http and https are allowed."
|
||||
max_bytes = 2 * 1024 * 1024
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "agent/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
content_type = resp.headers.get_content_type()
|
||||
if content_type and content_type not in (
|
||||
"text/html",
|
||||
"text/plain",
|
||||
"application/xhtml+xml",
|
||||
):
|
||||
return f"Error fetching {url}: unsupported content type '{content_type}'."
|
||||
charset = resp.headers.get_content_charset() or "utf-8"
|
||||
raw_chunks = []
|
||||
total = 0
|
||||
while True:
|
||||
chunk = resp.read(65536)
|
||||
if not chunk:
|
||||
break
|
||||
total += len(chunk)
|
||||
if total > max_bytes:
|
||||
raw_chunks.append(chunk[: max_bytes - (total - len(chunk))])
|
||||
break
|
||||
raw_chunks.append(chunk)
|
||||
raw = b"".join(raw_chunks).decode(charset, errors="replace")
|
||||
soup = BeautifulSoup(raw, "html.parser")
|
||||
text = soup.get_text(separator="\n", strip=True)
|
||||
return re.sub(r"\n{3,}", "\n\n", text).strip()
|
||||
except Exception as e:
|
||||
return f"Error fetching {url}: {e}"
|
||||
Reference in New Issue
Block a user