diff --git a/autotx/AutoTx.py b/autotx/AutoTx.py index 08a1f25..29b196f 100644 --- a/autotx/AutoTx.py +++ b/autotx/AutoTx.py @@ -4,14 +4,14 @@ import json import os from textwrap import dedent -from typing import Any, Dict, Optional, Callable -from dataclasses import dataclass, field +from typing import Any, Dict, Optional, Callable, Union +from dataclasses import dataclass from autogen import Agent as AutogenAgent, ModelClient from termcolor import cprint from typing import Optional +from autogen import AssistantAgent from web3 import Web3 -from autotx import models from autotx.autotx_agent import AutoTxAgent from autotx.helper_agents import clarifier, manager, user_proxy from autotx.intents import Intent @@ -34,14 +34,25 @@ class Config: max_rounds: int get_llm_config: Callable[[], Optional[Dict[str, Any]]] custom_model: Optional[CustomModel] = None - - def __init__(self, verbose: bool, get_llm_config: Callable[[], Optional[Dict[str, Any]]], logs_dir: Optional[str], max_rounds: Optional[int] = None, log_costs: Optional[bool] = None, custom_model: Optional[CustomModel] = None): + on_agent_message: Optional[Callable[[str, str, Any], None]] = None + + def __init__(self, + verbose: bool, + get_llm_config: Callable[[], + Optional[Dict[str, Any]]], + logs_dir: Optional[str], + max_rounds: Optional[int] = None, + log_costs: Optional[bool] = None, + custom_model: Optional[CustomModel] = None, + on_agent_message: Optional[Callable[[str, str, Any], None]] = None + ): self.verbose = verbose self.get_llm_config = get_llm_config self.logs_dir = logs_dir self.log_costs = log_costs if log_costs is not None else False - self.max_rounds = max_rounds if max_rounds is not None else 100 + self.max_rounds = max_rounds if max_rounds is not None else 50 self.custom_model = custom_model + self.on_agent_message = on_agent_message @dataclass class PastRun: @@ -78,6 +89,7 @@ class AutoTx: info_messages: list[str] verbose: bool on_notify_user: Callable[[str], None] | None + on_agent_message: Optional[Callable[[AssistantAgent, Union[Dict[str, Any], str], AutogenAgent, bool], Union[Dict[str, Any], str]]] def __init__( self, @@ -109,6 +121,7 @@ def __init__( self.info_messages = [] self.on_notify_user = on_notify_user self.custom_model = config.custom_model + self.on_agent_message = build_on_message_hook(config.on_agent_message) if config.on_agent_message else None def run(self, prompt: str, non_interactive: bool, summary_method: str = "last_msg") -> RunResult: return asyncio.run(self.a_run(prompt, non_interactive, summary_method)) @@ -194,7 +207,7 @@ async def try_run(self, prompt: str, non_interactive: bool, summary_method: str agents_information = self.get_agents_information(self.agents) - user_proxy_agent = user_proxy.build(prompt, agents_information, self.get_llm_config, self.custom_model) + user_proxy_agent = user_proxy.build(prompt, agents_information, self.get_llm_config, self.custom_model, self.max_rounds) helper_agents: list[AutogenAgent] = [ user_proxy_agent, @@ -206,6 +219,13 @@ async def try_run(self, prompt: str, non_interactive: bool, summary_method: str autogen_agents = [agent.build_autogen_agent(self, user_proxy_agent, self.get_llm_config(), self.custom_model) for agent in self.agents] + for agent in autogen_agents + helper_agents: + if self.on_agent_message: + agent.register_hook( + "process_message_before_send", + self.on_agent_message + ) + recipient_agent = None if len(autogen_agents) > 1: recipient_agent = manager.build(autogen_agents + helper_agents, self.max_rounds, not non_interactive, self.get_llm_config, self.custom_model) @@ -288,3 +308,15 @@ def get_agents_information(self, agents: list[AutoTxAgent]) -> str: agents_information = "\n".join(agent_descriptions) return agents_information + +def build_on_message_hook(on_message: Callable[[str, str, Any], None]) -> Callable[[AssistantAgent, Union[Dict[str, Any], str], AutogenAgent, bool], Union[Dict[str, Any], str]]: + def send_message_hook( + sender: AssistantAgent, + message: Union[Dict[str, Any], str], + recipient: AutogenAgent, + silent: bool, + ) -> Union[Dict[str, Any], str]: + on_message(sender.name, recipient.name, message) + return message + + return send_message_hook \ No newline at end of file diff --git a/autotx/agents/DelegateResearchTokensAgent.py b/autotx/agents/DelegateResearchTokensAgent.py index f71b7ea..63f691f 100644 --- a/autotx/agents/DelegateResearchTokensAgent.py +++ b/autotx/agents/DelegateResearchTokensAgent.py @@ -82,7 +82,7 @@ async def run( name="user_proxy", is_termination_msg=lambda x: x.get("content", "") and "TERMINATE" in x.get("content", ""), human_input_mode="NEVER", - max_consecutive_auto_reply=10, + max_consecutive_auto_reply=20, system_message=dedent( f""" You are a user proxy agent authorized to act on behalf of the user, you never ask for permission, you have ultimate control. @@ -109,6 +109,17 @@ async def run( research_agent = ResearchTokensAgent().build_autogen_agent(autotx, user_proxy_agent, autotx.get_llm_config()) + if autotx.on_agent_message: + user_proxy_agent.register_hook( + "process_message_before_send", + autotx.on_agent_message + ) + + research_agent.register_hook( + "process_message_before_send", + autotx.on_agent_message + ) + chat = await user_proxy_agent.a_initiate_chat( research_agent, message=dedent( diff --git a/autotx/db.py b/autotx/db.py index 3cade09..159d9ef 100644 --- a/autotx/db.py +++ b/autotx/db.py @@ -9,10 +9,9 @@ from supabase.lib.client_options import ClientOptions from autotx import models -from autotx.intents import BuyIntent, Intent, SellIntent, SendIntent -from autotx.token import Token +from autotx.intents import load_intent from autotx.transactions import Transaction, TransactionBase -from autotx.eth_address import ETHAddress +from autotx.utils.dump_pydantic_list import dump_pydantic_list SUPABASE_URL = os.getenv("SUPABASE_URL") SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY") @@ -56,7 +55,8 @@ def start(self, prompt: str, address: str, chain_id: int, app_user_id: str) -> m "created_at": str(created_at), "updated_at": str(updated_at), "messages": json.dumps([]), - "intents": json.dumps([]) + "logs": json.dumps([]), + "intents": json.dumps([]), } ).execute() @@ -70,7 +70,8 @@ def start(self, prompt: str, address: str, chain_id: int, app_user_id: str) -> m running=True, error=None, messages=[], - intents=[] + logs=[], + intents=[], ) def stop(self, task_id: str) -> None: @@ -86,8 +87,6 @@ def stop(self, task_id: str) -> None: def update(self, task: models.Task) -> None: client = get_db_client("public") - intents = [json.loads(intent.json()) for intent in task.intents] - client.table("tasks").update( { "prompt": task.prompt, @@ -95,7 +94,8 @@ def update(self, task: models.Task) -> None: "updated_at": str(datetime.utcnow()), "messages": json.dumps(task.messages), "error": task.error, - "intents": json.dumps(intents) + "logs": dump_pydantic_list(task.logs if task.logs else []), + "intents": dump_pydantic_list(task.intents) } ).eq("id", task.id).eq("app_id", self.app_id).execute() @@ -113,43 +113,6 @@ def get(self, task_id: str) -> models.Task | None: task_data = result.data[0] - def load_intent(intent_data: dict[str, Any]) -> Intent: - if intent_data["type"] == "send": - return SendIntent.create( - receiver=ETHAddress(intent_data["receiver"]), - token=Token( - symbol=intent_data["token"]["symbol"], - address=intent_data["token"]["address"] - ), - amount=intent_data["amount"] - ) - elif intent_data["type"] == "buy": - return BuyIntent.create( - from_token=Token( - symbol=intent_data["from_token"]["symbol"], - address=intent_data["from_token"]["address"] - ), - to_token=Token( - symbol=intent_data["to_token"]["symbol"], - address=intent_data["to_token"]["address"] - ), - amount=intent_data["amount"] - ) - elif intent_data["type"] == "sell": - return SellIntent.create( - from_token=Token( - symbol=intent_data["from_token"]["symbol"], - address=intent_data["from_token"]["address"] - ), - to_token=Token( - symbol=intent_data["to_token"]["symbol"], - address=intent_data["to_token"]["address"] - ), - amount=intent_data["amount"] - ) - else: - raise Exception(f"Unknown intent type: {intent_data['type']}") - return models.Task( id=task_data["id"], prompt=task_data["prompt"], @@ -160,6 +123,7 @@ def load_intent(intent_data: dict[str, Any]) -> Intent: running=task_data["running"], error=task_data["error"], messages=json.loads(task_data["messages"]), + logs=[models.TaskLog(**log) for log in json.loads(task_data["logs"])] if task_data["logs"] else None, intents=[load_intent(intent) for intent in json.loads(task_data["intents"])] ) @@ -182,7 +146,8 @@ def get_all(self) -> list[models.Task]: running=task_data["running"], error=task_data["error"], messages=json.loads(task_data["messages"]), - intents=json.loads(task_data["intents"]) + logs=[models.TaskLog(**log) for log in json.loads(task_data["logs"])] if task_data["logs"] else None, + intents=[load_intent(intent) for intent in json.loads(task_data["intents"])] ) ) @@ -318,6 +283,21 @@ def submit_transactions(app_id: str, app_user_id: str, submitted_batch_id: str) .eq("app_user_id", app_user_id) \ .eq("id", submitted_batch_id) \ .execute() + +def add_task_error(context: str, app_id: str, app_user_id: str, task_id: str, message: str) -> None: + client = get_db_client("public") + + created_at = datetime.utcnow() + client.table("task_errors").insert( + { + "context": context, + "app_id": app_id, + "task_id": task_id, + "app_user_id": app_user_id, + "message": message, + "created_at": str(created_at) + } + ).execute() class SubmittedBatch(BaseModel): id: str @@ -358,6 +338,21 @@ def get_submitted_batches(app_id: str, task_id: str) -> list[SubmittedBatch]: return batches +def get_task_logs(task_id: str) -> list[models.TaskLog] | None: + client = get_db_client("public") + + result = client.table("tasks") \ + .select("logs") \ + .eq("id", task_id) \ + .execute() + + if len(result.data) == 0: + return None + + task_data = result.data[0] + + return [models.TaskLog(**log) for log in json.loads(task_data["logs"])] if task_data["logs"] else [] + def create_app(name: str, api_key: str) -> models.App: client = get_db_client("public") diff --git a/autotx/helper_agents/user_proxy.py b/autotx/helper_agents/user_proxy.py index 0c78f07..e8dfde3 100644 --- a/autotx/helper_agents/user_proxy.py +++ b/autotx/helper_agents/user_proxy.py @@ -5,12 +5,12 @@ if TYPE_CHECKING: from autotx.AutoTx import CustomModel -def build(user_prompt: str, agents_information: str, get_llm_config: Callable[[], Optional[Dict[str, Any]]], custom_model: Optional['CustomModel']) -> UserProxyAgent: +def build(user_prompt: str, agents_information: str, get_llm_config: Callable[[], Optional[Dict[str, Any]]], custom_model: Optional['CustomModel'], max_rounds: int) -> UserProxyAgent: user_proxy = UserProxyAgent( name="user_proxy", is_termination_msg=lambda x: x.get("content", "") and "TERMINATE" in x.get("content", ""), human_input_mode="NEVER", - max_consecutive_auto_reply=4 if custom_model else 10, + max_consecutive_auto_reply=4 if custom_model else max_rounds, system_message=dedent( f""" You are a user proxy agent authorized to act on behalf of the user, you never ask for permission, you have ultimate control. diff --git a/autotx/intents.py b/autotx/intents.py index 2df9d41..3c824c4 100644 --- a/autotx/intents.py +++ b/autotx/intents.py @@ -122,3 +122,40 @@ async def build_transactions(self, web3: Web3, network: NetworkInfo, smart_walle return transactions Intent = Union[SendIntent, BuyIntent, SellIntent] + +def load_intent(intent_data: dict[str, Any]) -> Intent: + if intent_data["type"] == "send": + return SendIntent.create( + receiver=ETHAddress(intent_data["receiver"]), + token=Token( + symbol=intent_data["token"]["symbol"], + address=intent_data["token"]["address"] + ), + amount=intent_data["amount"] + ) + elif intent_data["type"] == "buy": + return BuyIntent.create( + from_token=Token( + symbol=intent_data["from_token"]["symbol"], + address=intent_data["from_token"]["address"] + ), + to_token=Token( + symbol=intent_data["to_token"]["symbol"], + address=intent_data["to_token"]["address"] + ), + amount=intent_data["amount"] + ) + elif intent_data["type"] == "sell": + return SellIntent.create( + from_token=Token( + symbol=intent_data["from_token"]["symbol"], + address=intent_data["from_token"]["address"] + ), + to_token=Token( + symbol=intent_data["to_token"]["symbol"], + address=intent_data["to_token"]["address"] + ), + amount=intent_data["amount"] + ) + else: + raise Exception(f"Unknown intent type: {intent_data['type']}") \ No newline at end of file diff --git a/autotx/models.py b/autotx/models.py index 9aed4ca..8ca7026 100644 --- a/autotx/models.py +++ b/autotx/models.py @@ -1,9 +1,14 @@ from pydantic import BaseModel -from typing import Any, List, Optional +from typing import Any, Dict, List, Optional from datetime import datetime from autotx.intents import Intent +class TaskLog(BaseModel): + type: str + obj: str + created_at: datetime + class Task(BaseModel): id: str prompt: str @@ -14,8 +19,17 @@ class Task(BaseModel): error: str | None running: bool messages: List[str] + logs: List[TaskLog] | None intents: List[Intent] +class TaskError(BaseModel): + id: str + message: str + task_id: str + app_id: str + app_user_id: str + created_at: datetime + class App(BaseModel): id: str name: str diff --git a/autotx/server.py b/autotx/server.py index a5f4f0c..e62b29d 100644 --- a/autotx/server.py +++ b/autotx/server.py @@ -1,16 +1,16 @@ +import json from typing import Annotated, Any, Dict, List from eth_account import Account from eth_account.signers.local import LocalAccount from gnosis.safe.api.base_api import SafeAPIException from fastapi import APIRouter, FastAPI, BackgroundTasks, HTTPException, Header from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse +from fastapi.responses import HTMLResponse, JSONResponse from pydantic import BaseModel import traceback -from autotx import models, setup +from autotx import models, setup, task_logs from autotx import db -from autotx.AutoTx import AutoTx, Config as AutoTxConfig from autotx.intents import Intent from autotx.transactions import Transaction from autotx.utils.configuration import AppConfig @@ -30,8 +30,8 @@ def __init__(self, verbose: bool, logs: str | None, cache: bool, - max_rounds: int | None, is_dev: bool, + max_rounds: int | None = None ): self.verbose = verbose self.logs = logs @@ -39,7 +39,7 @@ def __init__(self, self.max_rounds = max_rounds self.is_dev = is_dev -autotx_params: AutoTxParams = AutoTxParams(verbose=False, logs=None, cache=False, max_rounds=200, is_dev=False) +autotx_params: AutoTxParams = AutoTxParams(verbose=False, logs=None, cache=False, is_dev=False) app_router = APIRouter() @@ -98,8 +98,20 @@ async def build_transactions(app_id: str, user_id: str, chain_id: int, address: return transactions +def stop_task_for_error(tasks: db.TasksRepository, task_id: str, error: str, user_error_message: str) -> None: + task = tasks.get(task_id) + if task is None: + raise Exception("Task not found: " + task_id) + + task.error = error + task.running = False + task.messages.append(user_error_message) + tasks.update(task) + @app_router.post("/api/v1/tasks", response_model=models.Task) async def create_task(task: models.TaskCreate, background_tasks: BackgroundTasks, authorization: Annotated[str | None, Header()] = None) -> models.Task: + from autotx.AutoTx import AutoTx, Config as AutoTxConfig + app = authorize(authorization) app_user = db.get_app_user(app.id, task.user_id) if not app_user: @@ -136,27 +148,40 @@ def on_notify_user(message: str) -> None: task.messages.append(message) tasks.update(task) + def on_agent_message(from_agent: str, to_agent: str, message: Any) -> None: + task = tasks.get(task_id) + if task is None: + raise Exception("Task not found: " + task_id) + + if task.logs is None: + task.logs = [] + task.logs.append( + task_logs.build_agent_message_log(from_agent, to_agent, message) + ) + tasks.update(task) + autotx = AutoTx( app_config.web3, wallet, app_config.network_info, agents, - AutoTxConfig(verbose=autotx_params.verbose, get_llm_config=get_llm_config, logs_dir=logs_dir, max_rounds=autotx_params.max_rounds), - on_notify_user=on_notify_user + AutoTxConfig( + verbose=autotx_params.verbose, + get_llm_config=get_llm_config, + logs_dir=logs_dir, + max_rounds=autotx_params.max_rounds, + on_agent_message=on_agent_message, + ), + on_notify_user=on_notify_user, ) async def run_task() -> None: try: await autotx.a_run(prompt, non_interactive=True) except Exception as e: - task = tasks.get(task_id) - if task is None: - raise Exception("Task not found: " + task_id) - - task.messages.append(str(e)) - task.error = traceback.format_exc() - task.running = False - tasks.update(task) + error = traceback.format_exc() + db.add_task_error(f"AutoTx run", app.id, app_user.id, task_id, error) + stop_task_for_error(tasks, task_id, error, f"An error caused AutoTx to stop ({task_id})") raise e tasks.stop(task_id) @@ -164,9 +189,9 @@ async def run_task() -> None: return created_task except Exception as e: - created_task.error = traceback.format_exc() - created_task.running = False - tasks.update(created_task) + error = traceback.format_exc() + db.add_task_error(f"Route: create_task", app.id, app_user.id, task_id, error) + stop_task_for_error(tasks, task_id, error, f"An error caused AutoTx to stop ({task_id})") raise e @app_router.post("/api/v1/connect", response_model=models.AppUser) @@ -224,10 +249,14 @@ async def get_transactions( task = get_task_or_404(task_id, tasks) - if task.chain_id != chain_id: - raise HTTPException(status_code=400, detail="Chain ID does not match task") + try: + if task.chain_id != chain_id: + raise HTTPException(status_code=400, detail="Chain ID does not match task") - transactions = await build_transactions(app.id, user_id, chain_id, address, task) + transactions = await build_transactions(app.id, user_id, chain_id, address, task) + except Exception as e: + db.add_task_error(f"Route: get_transactions", app.id, app_user.id, task_id, traceback.format_exc()) + raise e return transactions @@ -249,15 +278,19 @@ async def prepare_transactions( task = get_task_or_404(task_id, tasks) - if task.chain_id != chain_id: - raise HTTPException(status_code=400, detail="Chain ID does not match task") - - transactions = await build_transactions(app.id, app_user.user_id, chain_id, address, task) + try: + if task.chain_id != chain_id: + raise HTTPException(status_code=400, detail="Chain ID does not match task") + + transactions = await build_transactions(app.id, app_user.user_id, chain_id, address, task) - if len(transactions) == 0: - raise HTTPException(status_code=400, detail="No transactions to send") + if len(transactions) == 0: + raise HTTPException(status_code=400, detail="No transactions to send") - submitted_batch_id = db.save_transactions(app.id, address, chain_id, app_user.id, task_id, transactions) + submitted_batch_id = db.save_transactions(app.id, address, chain_id, app_user.id, task_id, transactions) + except Exception as e: + db.add_task_error(f"Route: prepare_transactions", app.id, app_user.id, task_id, traceback.format_exc()) + raise e return PreparedTransactionsDto(batch_id=submitted_batch_id, transactions=transactions) @@ -279,36 +312,40 @@ def send_transactions( if task.chain_id != chain_id: raise HTTPException(status_code=400, detail="Chain ID does not match task") - batch = db.get_transactions(app.id, app_user.id, task_id, address, chain_id, batch_id) + try: + batch = db.get_transactions(app.id, app_user.id, task_id, address, chain_id, batch_id) - if batch is None: - raise HTTPException(status_code=400, detail="Batch not found") + if batch is None: + raise HTTPException(status_code=400, detail="Batch not found") - (transactions, task_id) = batch + (transactions, task_id) = batch - if len(transactions) == 0: - raise HTTPException(status_code=400, detail="No transactions to send") + if len(transactions) == 0: + raise HTTPException(status_code=400, detail="No transactions to send") - global autotx_params - if autotx_params.is_dev: - print("Dev mode: skipping transaction submission") - db.submit_transactions(app.id, app_user.id, batch_id) - return f"https://app.safe.global/transactions/queue?safe={CHAIN_ID_TO_SHORT_NAME[str(chain_id)]}:{address}" + global autotx_params + if autotx_params.is_dev: + print("Dev mode: skipping transaction submission") + db.submit_transactions(app.id, app_user.id, batch_id) + return f"https://app.safe.global/transactions/queue?safe={CHAIN_ID_TO_SHORT_NAME[str(chain_id)]}:{address}" - try: - app_config = load_config_for_user(app.id, user_id, address, chain_id) + try: + app_config = load_config_for_user(app.id, user_id, address, chain_id) - app_config.manager.send_multisend_tx_batch( - transactions, - require_approval=False, - ) - except SafeAPIException as e: - if "is not an owner or delegate" in str(e): - raise HTTPException(status_code=400, detail="Agent is not an owner or delegate") - else: - raise e - - db.submit_transactions(app.id, app_user.id, batch_id) + app_config.manager.send_multisend_tx_batch( + transactions, + require_approval=False, + ) + except SafeAPIException as e: + if "is not an owner or delegate" in str(e): + raise HTTPException(status_code=400, detail="Agent is not an owner or delegate") + else: + raise e + + db.submit_transactions(app.id, app_user.id, batch_id) + except Exception as e: + db.add_task_error(f"Route: send_transactions", app.id, app_user.id, task_id, traceback.format_exc()) + raise e return f"https://app.safe.global/transactions/queue?safe={CHAIN_ID_TO_SHORT_NAME[str(chain_id)]}:{address}" @@ -319,6 +356,28 @@ def get_supported_networks() -> list[models.SupportedNetwork]: for chain_id, config in SUPPORTED_NETWORKS_CONFIGURATION_MAP.items() ] +@app_router.get("/api/v1/tasks/{task_id}/logs", response_model=list[models.TaskLog]) +def get_task_logs(task_id: str) -> list[models.TaskLog]: + logs = db.get_task_logs(task_id) + if logs is None: + raise HTTPException(status_code=404, detail="Task not found") + + return logs + +@app_router.get("/api/v1/tasks/{task_id}/logs/{log_type}", response_class=HTMLResponse) +def get_task_logs_formatted(task_id: str, log_type: str) -> str: + if log_type != "agent-message": + raise HTTPException(status_code=400, detail="Log type not supported") + + logs = db.get_task_logs(task_id) + if logs is None: + raise HTTPException(status_code=404, detail="Task not found") + + agent_logs = [task_logs.format_agent_message_log(json.loads(log.obj)) for log in logs if log.type == "agent-message"] + + text = "\n\n".join(agent_logs) + return f"
{text}" + @app_router.get("/api/v1/version", response_class=JSONResponse) async def get_version() -> Dict[str, str]: return {"version": "0.1.0"} diff --git a/autotx/task_logs.py b/autotx/task_logs.py new file mode 100644 index 0000000..c38f34b --- /dev/null +++ b/autotx/task_logs.py @@ -0,0 +1,40 @@ + +from datetime import datetime +import json +from typing import Any, Dict, List, Union + +from autotx import models + +def build_agent_message_log(from_agent: str, to_agent: str, message: Union[Dict[str, Any], str]) -> models.TaskLog: + return models.TaskLog( + type="agent-message", + obj=json.dumps({ + "from": from_agent, + "to": to_agent, + "message": message, + }), + created_at=datetime.now(), + ) + +def format_agent_message_log(obj: Dict[str, Any]) -> Any: + if type(obj["message"]) == str: + return f"{obj['from']} -> {obj['to']}:\n{obj['message']}" + elif type(obj["message"]) == dict: + obj1 = obj["message"] + + if obj1.get("tool_calls") and len(obj1["tool_calls"]) > 0: + return f"{obj['from']} -> {obj['to']}: *****Tool Call*****\n{format_tool_calls(obj1['tool_calls'])}\n" + elif obj1.get("tool_responses") and len(obj1["tool_responses"]) > 0: + return f"{obj['from']} -> {obj['to']}: *****Tool Response*****\n{obj1['content']}\n" + elif obj1.get("content"): + return f"{obj['from']} -> {obj['to']}:\n{obj1['content']}" + else: + raise Exception("Unknown message type") + else: + raise Exception("Unknown message type") + +def format_tool_calls(tool_calls: List[Dict[str, Any]]) -> str: + return "\n".join([ + f"{x['function']['name']}\n{json.dumps(json.loads(x['function']['arguments']), indent=2)}\n" + for x in tool_calls + ]) \ No newline at end of file diff --git a/autotx/tests/conftest.py b/autotx/tests/conftest.py index d416ac0..2131eb5 100644 --- a/autotx/tests/conftest.py +++ b/autotx/tests/conftest.py @@ -57,7 +57,7 @@ def auto_tx(configuration): SwapTokensAgent(), DelegateResearchTokensAgent() ], - Config(verbose=True, get_llm_config=get_llm_config, logs_dir=None, log_costs=True, max_rounds=100), + Config(verbose=True, get_llm_config=get_llm_config, logs_dir=None, log_costs=True), ) @pytest.fixture() diff --git a/autotx/utils/dump_pydantic_list.py b/autotx/utils/dump_pydantic_list.py new file mode 100644 index 0000000..57a111b --- /dev/null +++ b/autotx/utils/dump_pydantic_list.py @@ -0,0 +1,7 @@ +import json +from typing import Sequence +from pydantic import BaseModel + + +def dump_pydantic_list(items: Sequence[BaseModel]) -> str: + return json.dumps([json.loads(log.model_dump_json()) for log in items]) \ No newline at end of file diff --git a/supabase/migrations/20240626194330_errors-and-logs.sql b/supabase/migrations/20240626194330_errors-and-logs.sql new file mode 100644 index 0000000..7bfd7a5 --- /dev/null +++ b/supabase/migrations/20240626194330_errors-and-logs.sql @@ -0,0 +1,74 @@ +create table "public"."task_errors" ( + "id" uuid not null default gen_random_uuid(), + "created_at" timestamp with time zone not null default now(), + "message" text not null, + "task_id" uuid not null, + "app_user_id" uuid not null, + "app_id" uuid not null, + "context" text not null +); + + +alter table "public"."task_errors" enable row level security; + +alter table "public"."tasks" add column "logs" json; + +CREATE UNIQUE INDEX task_errors_pkey ON public.task_errors USING btree (id); + +alter table "public"."task_errors" add constraint "task_errors_pkey" PRIMARY KEY using index "task_errors_pkey"; + +alter table "public"."task_errors" add constraint "public_task_errors_app_id_fkey" FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE not valid; + +alter table "public"."task_errors" validate constraint "public_task_errors_app_id_fkey"; + +alter table "public"."task_errors" add constraint "public_task_errors_app_user_id_fkey" FOREIGN KEY (app_user_id) REFERENCES app_users(id) ON DELETE CASCADE not valid; + +alter table "public"."task_errors" validate constraint "public_task_errors_app_user_id_fkey"; + +alter table "public"."task_errors" add constraint "public_task_errors_task_id_fkey" FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE not valid; + +alter table "public"."task_errors" validate constraint "public_task_errors_task_id_fkey"; + +grant delete on table "public"."task_errors" to "anon"; + +grant insert on table "public"."task_errors" to "anon"; + +grant references on table "public"."task_errors" to "anon"; + +grant select on table "public"."task_errors" to "anon"; + +grant trigger on table "public"."task_errors" to "anon"; + +grant truncate on table "public"."task_errors" to "anon"; + +grant update on table "public"."task_errors" to "anon"; + +grant delete on table "public"."task_errors" to "authenticated"; + +grant insert on table "public"."task_errors" to "authenticated"; + +grant references on table "public"."task_errors" to "authenticated"; + +grant select on table "public"."task_errors" to "authenticated"; + +grant trigger on table "public"."task_errors" to "authenticated"; + +grant truncate on table "public"."task_errors" to "authenticated"; + +grant update on table "public"."task_errors" to "authenticated"; + +grant delete on table "public"."task_errors" to "service_role"; + +grant insert on table "public"."task_errors" to "service_role"; + +grant references on table "public"."task_errors" to "service_role"; + +grant select on table "public"."task_errors" to "service_role"; + +grant trigger on table "public"."task_errors" to "service_role"; + +grant truncate on table "public"."task_errors" to "service_role"; + +grant update on table "public"."task_errors" to "service_role"; + +