mirror of https://github.com/kortix-ai/suna.git
state v1
This commit is contained in:
parent
cad8edd402
commit
46f60e8b57
26
agent.py
26
agent.py
|
@ -1,10 +1,13 @@
|
|||
import asyncio
|
||||
import json
|
||||
from agentpress.thread_manager import ThreadManager
|
||||
from tools.files_tool import FilesTool
|
||||
from state_manager import StateManager
|
||||
|
||||
async def run_agent(
|
||||
thread_manager: ThreadManager,
|
||||
thread_id: int,
|
||||
state_manager: StateManager,
|
||||
max_iterations: int = 10
|
||||
):
|
||||
|
||||
|
@ -28,9 +31,22 @@ async def run_agent(
|
|||
iteration += 1
|
||||
await pre_iteration()
|
||||
|
||||
system_message = {"role": "system", "content": "You are a helpful assistant that can create, read, update, and delete files."}
|
||||
# Get entire state store
|
||||
state = await state_manager.export_store()
|
||||
state_info = f"Current state store:\n{json.dumps(state, indent=2)}"
|
||||
|
||||
system_message = {
|
||||
"role": "system",
|
||||
"content": f"You are a helpful assistant that can create, read, update, and delete files.\n\n{state_info}"
|
||||
}
|
||||
model_name = "gpt-4o"
|
||||
|
||||
# Include entire state in additional message
|
||||
# additional_message = {
|
||||
# "role": "user",
|
||||
# "content": state_info
|
||||
# }
|
||||
|
||||
response = await thread_manager.run_thread(
|
||||
thread_id=thread_id,
|
||||
system_message=system_message,
|
||||
|
@ -38,7 +54,7 @@ async def run_agent(
|
|||
temperature=0.7,
|
||||
max_tokens=150,
|
||||
tool_choice="auto",
|
||||
additional_message=None,
|
||||
# additional_message=additional_message,
|
||||
execute_tools_async=False,
|
||||
use_tools=True,
|
||||
execute_model_tool_calls=True
|
||||
|
@ -52,7 +68,12 @@ async def run_agent(
|
|||
if __name__ == "__main__":
|
||||
async def main():
|
||||
thread_manager = ThreadManager()
|
||||
state_manager = StateManager("state.json")
|
||||
thread_id = await thread_manager.create_thread()
|
||||
|
||||
# Read current file contents
|
||||
files = await state_manager.get("files")
|
||||
|
||||
await thread_manager.add_message(thread_id, {"role": "user", "content": "Please create a file with a random name with the content 'Hello, world!'"})
|
||||
|
||||
thread_manager.add_tool(FilesTool)
|
||||
|
@ -60,6 +81,7 @@ if __name__ == "__main__":
|
|||
await run_agent(
|
||||
thread_manager=thread_manager,
|
||||
thread_id=thread_id,
|
||||
state_manager=state_manager,
|
||||
max_iterations=5
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"files": {
|
||||
"random_file_808.txt": "The only thing we have to fear is fear itself.",
|
||||
"random_file_202.txt": "All that glitters is not gold.",
|
||||
"random_file_606.txt": "You miss 100% of the shots you don't take.",
|
||||
"random_file_404.txt": "In the end, we only regret the chances we didn't take.",
|
||||
"random_file_1010.txt": "The greatest glory in living lies not in never falling, but in rising every time we fall.",
|
||||
"random_file_789.txt": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
"random_file_116.txt": "The pen is mightier than the sword.",
|
||||
"random_file_117.txt": "Beauty is in the eye of the beholder.",
|
||||
"random_file_303.txt": "A journey of a thousand miles begins with a single step.",
|
||||
"random_file_101.txt": "To be or not to be, that is the question.",
|
||||
"random_file_115.txt": "When in Rome, do as the Romans do.",
|
||||
"random_file_129.txt": "Hello, world!",
|
||||
"random_file_707.txt": "Not all those who wander are lost.",
|
||||
"random_file_505.txt": "The best time to plant a tree was 20 years ago. The second best time is now.",
|
||||
"random_file_909.txt": "Do not go where the path may lead, go instead where there is no path and leave a trail.",
|
||||
"random_file_128.txt": "In the middle of difficulty lies opportunity",
|
||||
"random_file_114.txt": "A picture is worth a thousand words.",
|
||||
"random_file_138.txt": "May",
|
||||
"random_file_111.txt": "Hello, world!",
|
||||
"random_file_139.txt": "Success usually comes to those who are too busy to be looking for it.",
|
||||
"random_file_113.txt": "Actions speak louder than words.",
|
||||
"random_file_112.txt": "The early bird catches the worm.",
|
||||
"random_file_137.txt": "Life is really simple, but we insist on making it complicated.",
|
||||
"random_file_123.txt": "Hello, world!",
|
||||
"random_file_122.txt": "Life is what happens when you're busy making other plans.",
|
||||
"random_file_136.txt": "The way to get started is to quit talking and begin doing.",
|
||||
"random_file_120.txt": "The only limit to our realization of tomorrow is our doubts of today.",
|
||||
"random_file_134.txt": "What lies behind us and what lies before us",
|
||||
"random_file_135.txt": "To succeed in life, you need three things: a wishbone, a backbone and a funny bone.",
|
||||
"random_file_121.txt": "The future belongs to those who believe in the beauty of their dreams.",
|
||||
"random_file_119.txt": "When the going gets tough, the tough get going.",
|
||||
"random_file_125.txt": "Happiness is not something ready-made. It comes from your own actions.",
|
||||
"random_file_131.txt": "Keep your face always toward the sunshine\u2014and shadows will fall behind you.",
|
||||
"random_file_124.txt": "The purpose of our lives is to be happy",
|
||||
"random_file_118.txt": "The early bird catches the worm.",
|
||||
"random_file_456.txt": "The quick brown fox jumps over the lazy dog.",
|
||||
"random_file_132.txt": "The best preparation for tomorrow is doing your best today.",
|
||||
"random_file_126.txt": "Turn your wounds into wisdom.",
|
||||
"random_file_127.txt": "Success is not how high you have climbed, but how you make a positive difference to the world.",
|
||||
"random_file_133.txt": "Nothing is impossible, the word itself says, \u2018I\u2019m possible!\u2019",
|
||||
"random_file_140.txt": "Don't be afraid to give up the good to go for the great.",
|
||||
"random_file_141.txt": "I find that the harder I work, the more luck I seem to have.",
|
||||
"random_file_143.txt": "The only impossible journey is the one you never begin.",
|
||||
"random_file_142.txt": "Success is not",
|
||||
"random_file_146.txt": "You must be the",
|
||||
"random_file_145.txt": "The best way to predict the future is to create it.",
|
||||
"random_file_144.txt": "In the end, we will remember not the words of our enemies, but the silence of our friends."
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
import json
|
||||
import os
|
||||
import logging
|
||||
from typing import Any
|
||||
from asyncio import Lock
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
class StateManager:
|
||||
def __init__(self, store_file: str = "state.json"):
|
||||
"""
|
||||
Initialize StateManager with custom store file name.
|
||||
|
||||
Args:
|
||||
store_file: Name of the JSON file to store state (default: "state.json")
|
||||
"""
|
||||
self.lock = Lock()
|
||||
self.store_file = store_file
|
||||
logging.info(f"StateManager initialized with store file: {store_file}")
|
||||
|
||||
@asynccontextmanager
|
||||
async def store_scope(self):
|
||||
try:
|
||||
# Read current state
|
||||
if os.path.exists(self.store_file):
|
||||
with open(self.store_file, 'r') as f:
|
||||
store = json.load(f)
|
||||
else:
|
||||
store = {}
|
||||
|
||||
yield store
|
||||
|
||||
# Write updated state
|
||||
with open(self.store_file, 'w') as f:
|
||||
json.dump(store, f, indent=2)
|
||||
logging.debug("Store saved successfully")
|
||||
except Exception as e:
|
||||
logging.error("Error in store operation", exc_info=True)
|
||||
raise
|
||||
|
||||
async def set(self, key: str, data: Any):
|
||||
"""
|
||||
Store any JSON-serializable data with a simple key.
|
||||
|
||||
Args:
|
||||
key: Simple string key like "config" or "settings"
|
||||
data: Any JSON-serializable data (dict, list, str, int, bool, etc)
|
||||
"""
|
||||
async with self.lock:
|
||||
async with self.store_scope() as store:
|
||||
try:
|
||||
store[key] = data # Will be JSON serialized when written to file
|
||||
logging.info(f'Updated store key: {key}')
|
||||
return data
|
||||
except Exception as e:
|
||||
logging.error(f'Error in set: {str(e)}')
|
||||
raise
|
||||
|
||||
async def get(self, key: str) -> Any:
|
||||
"""
|
||||
Get data for a key.
|
||||
|
||||
Args:
|
||||
key: Simple string key like "config" or "settings"
|
||||
"""
|
||||
async with self.store_scope() as store:
|
||||
if key in store:
|
||||
data = store[key]
|
||||
logging.info(f'Retrieved key: {key}')
|
||||
return data
|
||||
logging.info(f'Key not found: {key}')
|
||||
return None
|
||||
|
||||
async def delete(self, key: str):
|
||||
"""Delete data for a key"""
|
||||
async with self.lock:
|
||||
async with self.store_scope() as store:
|
||||
if key in store:
|
||||
del store[key]
|
||||
logging.info(f"Deleted key: {key}")
|
||||
else:
|
||||
logging.info(f"Key not found for deletion: {key}")
|
||||
|
||||
async def export_store(self) -> dict:
|
||||
"""Export entire store"""
|
||||
async with self.store_scope() as store:
|
||||
logging.info(f"Store content: {store}")
|
||||
return store
|
||||
|
||||
async def clear_store(self):
|
||||
"""Clear entire store"""
|
||||
async with self.lock:
|
||||
async with self.store_scope() as store:
|
||||
store.clear()
|
||||
logging.info("Cleared store")
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
{"messages": [{"role": "user", "content": "Please create a file with a random name with the content 'Hello, world!'"}, {"role": "assistant", "content": "", "tool_calls": [{"id": "call_NllUgRoFVHDhwFtuFcyZtbeQ", "type": "function", "function": {"name": "create_file", "arguments": "{\"file_path\":\"random_file_4812.txt\",\"content\":\"Hello, world!\"}"}}]}, {"role": "tool", "tool_call_id": "call_NllUgRoFVHDhwFtuFcyZtbeQ", "name": "create_file", "content": "ToolResult(success=True, output=\"File 'random_file_4812.txt' created successfully.\")"}, {"role": "user", "content": "CREATE MORE RANDOM FILES WITH RANDOM CONTENTS. JSUT CREATE IT \u2013 NO QUESTINS PLEASE.'"}, {"role": "assistant", "content": "", "tool_calls": [{"id": "call_HKp4nWYTRDSkVHivYTsHywL2", "type": "function", "function": {"name": "create_file", "arguments": "{\"file_path\": \"random_file_6573.txt\", \"content\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\"}"}}, {"id": "call_cJaBUWK5ybYgReV29niQb6hf", "type": "function", "function": {"name": "create_file", "arguments": "{\"file_path\": \"random_file_2910.txt\", \"content\": \"The quick brown fox jumps over the lazy dog.\"}"}}, {"id": "call_2WFuVQoDrkF3ayX6fMSlbJpn", "type": "function", "function": {"name": "create_file", "arguments": "{\"file_path\": \"random_file_8347.txt\", \"content\": \"To be, or not to be, that is the question.\"}"}}]}, {"role": "tool", "tool_call_id": "call_HKp4nWYTRDSkVHivYTsHywL2", "name": "create_file", "content": "ToolResult(success=True, output=\"File 'random_file_6573.txt' created successfully.\")"}, {"role": "tool", "tool_call_id": "call_cJaBUWK5ybYgReV29niQb6hf", "name": "create_file", "content": "ToolResult(success=True, output=\"File 'random_file_2910.txt' created successfully.\")"}, {"role": "tool", "tool_call_id": "call_2WFuVQoDrkF3ayX6fMSlbJpn", "name": "create_file", "content": "ToolResult(success=True, output=\"File 'random_file_8347.txt' created successfully.\")"}, {"role": "user", "content": "CREATE MORE RANDOM FILES WITH RANDOM CONTENTS. JSUT CREATE IT \u2013 NO QUESTINS PLEASE.'"}, {"role": "assistant", "content": "", "tool_calls": [{"id": "call_DjuN5efYZ9OcS6Z7Mz51wRdO", "type": "function", "function": {"name": "create_file", "arguments": "{\"file_path\": \"random_file_5021.txt\", \"content\": \"In the beginning God created the heaven and the earth.\"}"}}, {"id": "call_VWbUUI8yseHYELnYwkPW49xy", "type": "function", "function": {"name": "create_file", "arguments": "{\"file_path\": \"random_file_9384.txt\", \"content\": \"All human beings are born free and equal in dignity and rights.\"}"}}, {"id": "call_z6MV5G5YLX33YEq43rA5kwLt", "type": "function", "function": {"name": "create_file", "arguments": "{\"file_path\": \"random_file_1726.txt\", \"content\": \"The only thing we have to fear is fear itself.\"}"}}]}, {"role": "tool", "tool_call_id": "call_DjuN5efYZ9OcS6Z7Mz51wRdO", "name": "create_file", "content": "ToolResult(success=True, output=\"File 'random_file_5021.txt' created successfully.\")"}, {"role": "tool", "tool_call_id": "call_VWbUUI8yseHYELnYwkPW49xy", "name": "create_file", "content": "ToolResult(success=True, output=\"File 'random_file_9384.txt' created successfully.\")"}, {"role": "tool", "tool_call_id": "call_z6MV5G5YLX33YEq43rA5kwLt", "name": "create_file", "content": "ToolResult(success=True, output=\"File 'random_file_1726.txt' created successfully.\")"}, {"role": "user", "content": "CREATE MORE RANDOM FILES WITH RANDOM CONTENTS. JSUT CREATE IT \u2013 NO QUESTINS PLEASE.'"}]}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,12 +1,39 @@
|
|||
import os
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from agentpress.tool import Tool, ToolResult, tool_schema
|
||||
from state_manager import StateManager
|
||||
|
||||
class FilesTool(Tool):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.workspace = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'workspace')
|
||||
os.makedirs(self.workspace, exist_ok=True)
|
||||
self.state_manager = StateManager("state.json")
|
||||
asyncio.create_task(self._init_workspace_state())
|
||||
|
||||
async def _init_workspace_state(self):
|
||||
"""Initialize or update the workspace state in JSON"""
|
||||
files_state = {}
|
||||
|
||||
# Walk through workspace and record all files
|
||||
for root, _, files in os.walk(self.workspace):
|
||||
for file in files:
|
||||
full_path = os.path.join(root, file)
|
||||
# Get path relative to workspace: e.g. "src/components/file.jsx"
|
||||
rel_path = os.path.relpath(full_path, self.workspace)
|
||||
try:
|
||||
with open(full_path, 'r') as f:
|
||||
content = f.read()
|
||||
files_state[rel_path] = content
|
||||
except Exception as e:
|
||||
print(f"Error reading file {rel_path}: {e}")
|
||||
|
||||
await self.state_manager.set("files", files_state)
|
||||
|
||||
async def _update_workspace_state(self):
|
||||
"""Update the workspace state after any file operation"""
|
||||
await self._init_workspace_state()
|
||||
|
||||
@tool_schema({
|
||||
"name": "create_file",
|
||||
|
@ -25,9 +52,12 @@ class FilesTool(Tool):
|
|||
full_path = os.path.join(self.workspace, file_path)
|
||||
if os.path.exists(full_path):
|
||||
return self.fail_response(f"File '{file_path}' already exists. Use update_file to modify existing files.")
|
||||
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
with open(full_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
await self._update_workspace_state()
|
||||
return self.success_response(f"File '{file_path}' created successfully.")
|
||||
except Exception as e:
|
||||
return self.fail_response(f"Error creating file: {str(e)}")
|
||||
|
@ -45,10 +75,13 @@ class FilesTool(Tool):
|
|||
})
|
||||
async def read_file(self, file_path: str) -> ToolResult:
|
||||
try:
|
||||
full_path = os.path.join(self.workspace, file_path)
|
||||
with open(full_path, 'r') as f:
|
||||
content = f.read()
|
||||
return self.success_response({"file_path": file_path, "content": content})
|
||||
workspace_state = await self.state_manager.get("workspace")
|
||||
if file_path in workspace_state["files"]:
|
||||
return self.success_response({
|
||||
"file_path": file_path,
|
||||
"content": workspace_state["files"][file_path]["content"]
|
||||
})
|
||||
return self.fail_response(f"File '{file_path}' not found in workspace state.")
|
||||
except Exception as e:
|
||||
return self.fail_response(f"Error reading file: {str(e)}")
|
||||
|
||||
|
@ -69,11 +102,12 @@ class FilesTool(Tool):
|
|||
full_path = os.path.join(self.workspace, file_path)
|
||||
with open(full_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
await self._update_workspace_state()
|
||||
return self.success_response(f"File '{file_path}' updated successfully.")
|
||||
except Exception as e:
|
||||
return self.fail_response(f"Error updating file: {str(e)}")
|
||||
|
||||
|
||||
@tool_schema({
|
||||
"name": "delete_file",
|
||||
"description": "Delete a file from the workspace",
|
||||
|
@ -89,6 +123,8 @@ class FilesTool(Tool):
|
|||
try:
|
||||
full_path = os.path.join(self.workspace, file_path)
|
||||
os.remove(full_path)
|
||||
|
||||
await self._update_workspace_state()
|
||||
return self.success_response(f"File '{file_path}' deleted successfully.")
|
||||
except Exception as e:
|
||||
return self.fail_response(f"Error deleting file: {str(e)}")
|
||||
|
|
Loading…
Reference in New Issue