From 8a407efc27f8a49a572c06b6bffb2dfb2914accf Mon Sep 17 00:00:00 2001 From: marko-kraemer Date: Wed, 23 Oct 2024 03:42:38 +0200 Subject: [PATCH] wip --- LICENSE | 21 +++ README.md | 223 ++++++++++++++++++++-------- agent.py | 99 ++++++------ agentpress/llm.py | 6 +- main.db | Bin 176128 -> 53248 bytes tools/files_tool.py | 119 ++++++--------- tools/tool_example.py | 37 ----- workspace/AI_Discussion_Summary.txt | 5 - workspace/app.py | 14 -- workspace/main.py | 10 -- 10 files changed, 287 insertions(+), 247 deletions(-) create mode 100644 LICENSE delete mode 100644 tools/tool_example.py delete mode 100644 workspace/AI_Discussion_Summary.txt delete mode 100644 workspace/app.py delete mode 100644 workspace/main.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..ba8e8a13 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Kortix AI Corp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 1a1f29c4..aedf2917 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,178 @@ -# agentpress +# AgentPress -AgentPress simplifies the process of creating AI agents by providing a robust thread management system and a flexible tool integration mechanism. With AgentPress, you can easily create, configure, and run AI agents that can engage in conversations, perform tasks, and interact with various tools. +AgentPress is a powerful framework for creating AI agents, with the ThreadManager at its core. This system simplifies the process of building, configuring, and running AI agents that can engage in conversations, perform tasks, and interact with various tools. -### Key Features +## Key Concept: ThreadManager -- **Thread Management System**: Manage conversations and task executions through a sophisticated thread system. -- **Flexible Tool Integration**: Easily integrate and use custom tools within your AI agents. -- **Configurable Agent Behavior**: Fine-tune your agent's behavior with customizable settings and callbacks. -- **Autonomous Iterations**: Allow your agent to run multiple iterations autonomously. -- **State Management**: Control your agent's behavior at different stages of execution. +The ThreadManager is the central component of AgentPress. It manages conversation threads, handles tool integrations, and coordinates the execution of AI models. Here's why it's crucial: -### Getting Started +1. **Conversation Management**: It creates and manages threads, allowing for coherent multi-turn conversations. +2. **Tool Integration**: It integrates various tools that the AI can use to perform tasks. +3. **Model Execution**: It handles the execution of AI models, managing the context and responses. +4. **State Management**: It maintains the state of conversations and tool executions across multiple turns. -To get started with AgentPress, all you need to do is write your agent similar to the example in `agent.py`. Here's a basic outline: +## How It Works -1. Import necessary modules: - ```python - from agentpress.db import Database - from agentpress.thread_manager import ThreadManager - from tools.files_tool import FilesTool - ``` +1. **Create a ThreadManager**: This is your first step in using AgentPress. +2. **Add Tools**: Register any tools your agent might need. +3. **Create a Thread**: Each conversation or task execution is managed in a thread. +4. **Run the Thread**: Execute the AI model within the context of the thread, optionally using tools. -2. Create a ThreadManager instance: - ```python - db = Database() - manager = ThreadManager(db) - ``` +## Standalone Example -3. Set up your agent's configuration: - ```python - settings = { - "thread_id": thread_id, - "system_message": system_message, - "model_name": "gpt-4", - "temperature": 0.7, - "max_tokens": 150, - "autonomous_iterations_amount": 3, - "continue_instructions": "Continue the conversation...", - "tools": list(tool_schemas.keys()), - "tool_choice": "auto" - } - ``` +Here's how to use the ThreadManager standalone: -4. Define callback functions (optional): - ```python - def initializer(): - # Code to run at the start of the thread - - def pre_iteration(): - # Code to run before each iteration - - def after_iteration(): - # Code to run after each iteration - - def finalizer(): - # Code to run at the end of the thread - ``` +```python +import asyncio +from agentpress.thread_manager import ThreadManager +from tools.files_tool import FilesTool -5. Run your agent: - ```python - response = await manager.run_thread(settings) - ``` +async def main(): + # Create a ThreadManager instance + thread_manager = ThreadManager() -### Documentation + # Add a tool + thread_manager.add_tool(FilesTool) -The core of AgentPress is the `ThreadManager` class in `thread_manager.py`. It provides a comprehensive thread management system where you can: + # Create a new thread + thread_id = await thread_manager.create_thread() -- Create and manage threads -- Add messages to threads -- Run threads with specific settings -- Configure autonomous iterations -- Integrate and use tools + # Add an initial message to the thread + await thread_manager.add_message(thread_id, {"role": "user", "content": "Create a file named 'hello.txt' with the content 'Hello, World!'"}) -Tools in AgentPress are based on the `Tool` class defined in `tool.py`. You can create custom tools by inheriting from this class and implementing the required methods. An example is the `FilesTool` in `files_tool.py`. + # Run the thread + response = await thread_manager.run_thread( + thread_id=thread_id, + system_message={"role": "system", "content": "You are a helpful assistant that can create and manage files."}, + model_name="gpt-4", + temperature=0.7, + max_tokens=150, + tool_choice="auto" + ) -For more detailed documentation, please refer to the comments in the source code files. + # Print the response + print(response) + + # You can continue the conversation by adding more messages and running the thread again + await thread_manager.add_message(thread_id, {"role": "user", "content": "Now read the contents of 'hello.txt'"}) + + response = await thread_manager.run_thread( + thread_id=thread_id, + system_message={"role": "system", "content": "You are a helpful assistant that can create and manage files."}, + model_name="gpt-4", + temperature=0.7, + max_tokens=150, + tool_choice="auto" + ) + + print(response) + +if __name__ == "__main__": + asyncio.run(main()) +``` + +This example demonstrates how to: +1. Create a ThreadManager +2. Add a tool (FilesTool) +3. Create a new thread +4. Add messages to the thread +5. Run the thread, which executes the AI model and potentially uses tools +6. Continue the conversation with additional messages and thread runs + +## Building More Complex Agents + +While the ThreadManager can be used standalone, it's also the foundation for building more complex agents. You can create custom agent behaviors by defining initialization, pre-iteration, post-iteration, and finalization steps, setting up loops for autonomous iterations, and implementing custom logic for when and how to run threads. + +Here's an example of a more complex agent implementation using the `run_agent` function: + +```python +async def run_agent( + thread_manager: ThreadManager, + thread_id: int, + max_iterations: int = 10 +): + async def init(): + # Initialization code here + pass + + async def pre_iteration(): + # Pre-iteration code here + pass + + async def after_iteration(): + # Post-iteration code here + await thread_manager.add_message(thread_id, {"role": "user", "content": "CREATE MORE RANDOM FILES WITH RANDOM CONTENTS. JUST CREATE IT – NO QUESTIONS PLEASE."}) + + async def finalizer(): + # Finalization code here + pass + + await init() + + iteration = 0 + while iteration < max_iterations: + iteration += 1 + await pre_iteration() + + system_message = {"role": "system", "content": "You are a helpful assistant that can create, read, update, and delete files."} + model_name = "gpt-4" + + response = await thread_manager.run_thread( + thread_id=thread_id, + system_message=system_message, + model_name=model_name, + temperature=0.7, + max_tokens=150, + tool_choice="auto", + additional_message=None, + execute_tools_async=False, + execute_model_tool_calls=True + ) + + await after_iteration() + + await finalizer() + +# Usage +if __name__ == "__main__": + async def main(): + thread_manager = ThreadManager() + thread_id = await thread_manager.create_thread() + + 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) + + await run_agent( + thread_manager=thread_manager, + thread_id=thread_id, + max_iterations=5 + ) + + asyncio.run(main()) +``` + +This more complex example shows how to: +1. Define custom behavior for different stages of the agent's execution +2. Set up a loop for multiple iterations +3. Use the ThreadManager within a larger agent structure + +## Documentation + +For more detailed information about the AgentPress components: + +- `ThreadManager`: The core class that manages threads, tools, and model execution. +- `Tool`: Base class for creating custom tools that can be used by the AI. +- `ToolRegistry`: Manages the registration and retrieval of tools. + +Refer to the comments in the source code files for comprehensive documentation on each component. + +## Contributing + +We welcome contributions to AgentPress! Please feel free to submit issues, fork the repository and send pull requests! + +## License + +[MIT License](LICENSE) Built with ❤️ by [Kortix AI Corp](https://www.kortix.ai) diff --git a/agent.py b/agent.py index 5d628069..bbf22d42 100644 --- a/agent.py +++ b/agent.py @@ -1,60 +1,67 @@ import asyncio -from agentpress.db import Database +from typing import Dict, Any from agentpress.thread_manager import ThreadManager from tools.files_tool import FilesTool -async def run_agent(): - db = Database() - manager = ThreadManager(db) +async def run_agent( + thread_manager: ThreadManager, + thread_id: int, + max_iterations: int = 10 +): - thread_id = await manager.create_thread() - await manager.add_message(thread_id, {"role": "user", "content": "Let's have a conversation about artificial intelligence and create a file summarizing our discussion."}) - - system_message = {"role": "system", "content": "You are an AI expert engaging in a conversation about artificial intelligence. You can also create and manage files."} - - files_tool = FilesTool() - tool_schemas = files_tool.get_schemas() + async def init(): + pass - def initializer(): - print("Initializing thread run...") - manager.run_config['temperature'] = 0.8 + async def pre_iteration(): + pass - def pre_iteration(): - print(f"Preparing iteration {manager.current_iteration}...") - manager.run_config['max_tokens'] = 200 if manager.current_iteration > 3 else 150 + async def after_iteration(): + await thread_manager.add_message(thread_id, {"role": "user", "content": "CREATE MORE RANDOM FILES WITH RANDOM CONTENTS. JSUT CREATE IT – NO QUESTINS PLEASE.'"}) + pass - def after_iteration(): - print(f"Completed iteration {manager.current_iteration}. Status: {manager.run_config['status']}") - manager.run_config['continue_instructions'] = "Let's focus more on AI ethics in the next iteration and update our summary file." + async def finalizer(): + pass - def finalizer(): - print(f"Thread run finished with status: {manager.run_config['status']}") - print(f"Final configuration: {manager.run_config}") + await init() - settings = { - "thread_id": thread_id, - "system_message": system_message, - "model_name": "gpt-4", - "temperature": 0.7, - "max_tokens": 150, - "autonomous_iterations_amount": 3, - "continue_instructions": "Continue the conversation about AI, introducing new aspects or asking thought-provoking questions. Don't forget to update our summary file.", - "initializer": initializer, - "pre_iteration": pre_iteration, - "after_iteration": after_iteration, - "finalizer": finalizer, - "tools": list(tool_schemas.keys()), - "tool_choice": "auto" - } + iteration = 0 + while iteration < max_iterations: + iteration += 1 + await pre_iteration() - response = await manager.run_thread(settings) - - print(f"Thread run response: {response}") + system_message = {"role": "system", "content": "You are a helpful assistant that can create, read, update, and delete files."} + model_name = "gpt-4o" + + response = await thread_manager.run_thread( + thread_id=thread_id, + system_message=system_message, + model_name=model_name, + temperature=0.7, + max_tokens=150, + tool_choice="auto", + additional_message=None, + execute_tools_async=False, + execute_model_tool_calls=True + ) + + await after_iteration() + + await finalizer() - messages = await manager.list_messages(thread_id) - print("\nFinal conversation:") - for msg in messages: - print(f"{msg['role'].capitalize()}: {msg['content']}") if __name__ == "__main__": - asyncio.run(run_agent()) + async def main(): + thread_manager = ThreadManager() + thread_id = await thread_manager.create_thread() + + 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) + + await run_agent( + thread_manager=thread_manager, + thread_id=thread_id, + max_iterations=5 + ) + + asyncio.run(main()) diff --git a/agentpress/llm.py b/agentpress/llm.py index fa6b6cad..945d8910 100644 --- a/agentpress/llm.py +++ b/agentpress/llm.py @@ -26,7 +26,7 @@ os.environ['GROQ_API_KEY'] = GROQ_API_KEY logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -async def make_llm_api_call(messages, model_name, json_mode=False, temperature=0, max_tokens=None, tools=None, tool_choice="auto", api_key=None, api_base=None, agentops_session=None, stream=False, top_p=None, response_format=None) -> Union[Dict[str, Any], str]: +async def make_llm_api_call(messages, model_name, response_format=None, temperature=0, max_tokens=None, tools=None, tool_choice="auto", api_key=None, api_base=None, agentops_session=None, stream=False, top_p=None): litellm.set_verbose = True async def attempt_api_call(api_call_func, max_attempts=3): @@ -49,7 +49,7 @@ async def make_llm_api_call(messages, model_name, json_mode=False, temperature=0 "model": model_name, "messages": messages, "temperature": temperature, - "response_format": response_format or ({"type": "json_object"} if json_mode else None), + "response_format": response_format, "top_p": top_p, "stream": stream, } @@ -129,4 +129,4 @@ if __name__ == "__main__": # asyncio.run(test_llm_api_call(stream=True)) # For streaming # asyncio.run(test_llm_api_call(stream=False)) # For non-streaming - asyncio.run(test_llm_api_call()) \ No newline at end of file + asyncio.run(test_llm_api_call()) diff --git a/main.db b/main.db index b37b77e4b185ff9d1a6ce047675f12401ddb2bfa..e5811e6f72b197ff016f1856b8c9f4f5696d037e 100644 GIT binary patch literal 53248 zcmeHQU2G%Qb(VIiwO9KiJBfFlI8J8e6xww(>iqxMbsDWi{r^xRsXxn*?#$dFDUlqC zKl*cxVv}`Y*AGd5l83ZFUfKeENSmjkMbN(VA!vaFMPFK=Xo02y3bg4%`(QN9LwoL> z8H(geqoG35tD1{ltwdc8&pG#;bI*iJEr%8c0aTu*Or&zBjdr>E!p z@b8xQuNVF&|03|u|3iL0@6Y#pMt1(@*1&)FTp8ed2Ka%$I`0kL-*+X@l|WYlT?uq0 z(3L<}0$mAoCD4^XR{~uLbS3aqNZ`>6z0cfz>80MU^p=s4#pQGnBdg^7cV%ojI+}tFh(j`O)QdW;VLc+^;_MLH`5x%C)%foPm&nU4`19y) zUtjVOitwVOW1Y8ITj6};S^Rvi6J@g{5Die5v51Js3tPtvJQC2pWv)zMF(2P6M%Tx1 z#aN?nJ1S7|+LgRn&NzAUTJ`(a;IoN5>654iZV?%iF{qfT_^R@-sYxePUu4Rg zoYWv%{YkAOrHLsWdV6y-&oK2G3O_ces5{1cF6b$YXi!vm%sWFc>BLpU-kZM)u3|o- zh$b)Vy2%PM&w&%u6;{(VfmKCCH8@%1BultVu3}C~?e8Ws$?OC!9V34 z(i+!bE}}OrbrnIp_*&IKYOr2Spls@YdU z%GlnBOg=?dtWZ{-yfZyfnUPgzqReM;Vx=jqX)w|z^rof$B#0MZs~SiR)~k=TKgmrl&L43r zQgL6M+euXnv8bivII-H4X4DyR6ME64KMA7MpZPjcg6F;c{^b3~VsBsay-!CZ&el~d z@vLB5IxAsAWeriaSXophgas1`mVTMylUZ@Y%5Ur)&y8hOJ)Jt5Ib0dFlCkDip#~#u zLT_5?Pl9;ywW@*CCiLowR0dM_{#W43^?UH;+0Vh3XI_Rc*Pe$jeb?bjB+?IGztI0i z&+XKW%>n7=gIiNKe=u<4=7+cctp863{{F@v-TM5^w{O4D|Fs)`ck3_v|M}KW27c}K zmu|c}@Yc=cetY1LZ|&XwdjA_YfA98>Zhh-U{|)gbJgxhuD}k;Ax)SJ0peuo{1iBLF zO5p#k1nyr=_996VEUaj(Zi`Sk5et&Xqc4@4Y5!k!mNxXg)@0W6HR-N43s!cPz4!^ zNKjN_B@SU$w`2o~Hb^#1)zUOuxkm;noMh-GkY_3qKrE&zvl zqsW47sOD$MKnWfvN~#9TAP_ib+pJ-!B8!l%A=$z_$MG+bfwH3LqA1F&YRW*Ph=5uI zgsQS22vBVU)`icIfwn2xK)lRyGDt>}L>tO-lFf3wBrCjVTB40VO$G`QheZT)4GEfdEEp((Fo9~35osJUG>H{8&}&IoK+Sm2It^o!S26s`l0HE{TGbKaRfFZod`PyAKu|=ioWQB3 zW^yJ%&y#@~)^#MQJPXf-Cvs2>)sfA!qM+-VrW0$BpCbcJ9)sFRI_M*jqpc|*M_yw^ z+mZkq>Q6Wh`JPaPEiLzwfCIb~-5y5H{))Y(w)Xaj( zltD+JGKR3B+p=O9x9AfEtO8m91|Y-iNeb93GIR;7$ChnHmUu;yZ_+1fDpokmv7&{6 zYXMAJvjvOQRj_lU!i3Al4Kk2}S_yA}%@~>u0=I}O5Mb5?$rhmeD=DTT_LG6S#fg>* zBD5?76&9Vy8c7O^v5rJtMYgST*U3Qm!4xba8BvFU8i*dI2nKS7XkyTPc>l9xpkZ5r zZ6lNA3=Re=5}1nt)n{y~z_I|-u8GgkCxT(A3J9Ed3|5j2&@j<3SQ~@$k}TP>Ozs*P z7^alnM+SzeEJw(|FmAiKzwkx}chQ`I5B?1@UqreEnlxuM4WFht`Y^`I8`8 z{h6;LrHQ&qxBg$(gGtM^XSW^()yzX{O+yi^iFK%@^|~Nle64CAHCV4c*8U`@rXE@| z>WsJvy=XE%38K}X`8ra9=e=A1hyLM}{)j(u@l-1zSGg*~^GgE(d^OVFmwfm2NEl@40xd6e z4*_C|C~rAAy2~KMwt^N!b;FG{3#01@mwuFOQm;hkJ9Q<=x|bAudiW z?_?IHr1s52Lx*|`H&F!fav{&t_DF`P*1tVodzPwJ7^L?ysn+p$Za=xYfBbMYt81z3 z=&mp=CB_cy%cxpGyga?C^@oqwo}sE0rZ#YyRBI+(ERLu5$M;v|$q9L}#7#`@9OaMc zPP)q$F9q@P^s3g<d$-~DNSVe?LeU) z{E+1T&-T91bL)FI%KiWF?6)?OH--zh93CAsTu*MOdwgUJyx3!Tzf${UBaGmR6j?M9w)_%$rN&ZCt)8HsF9^tX6&?*$fP|H=Q%EUS9!+!VCodN0|!!!YQN`1gR zvfp`Z-UhusiiBx8>=a?OlYK$=Xzoa>(G4}TXdcGwcqxCdkvW`Nn#;9aujxT5d>a%y zh@%TB_UWKst#hPo3^707p$MR5!#{icrCXq7zZePAv3iNLY;9KCSV&Hog(Yo9+)FK_ z=U3I;^kj2~v@|Se!2#_P(-RKbueGSa6*!2a3u#$hvGlYR*i*8=($?^YkM7*0{uBCp z&+TOHlmZdW{@cgnH)wdNgy@{^6k)ZqeL?>z%*wfSRXIX-MH=6?aB6gC78y%(o$;Tt zCJJqEp+QWwN4b4Q&WR6(Uwr%!s-N$v-Rh@zj9G0L>%|>pTauKZif((f$gAR>qeNf) zDarqD_Wpa%t#96#zwvVafAlA=fA5*+BENn0Pp-avLu{LR znmk^-QpER|DO|)uCI7U+>7jl907A>{2dHlmAlW6#(nH`dMN$wqdT9agef7mz;%+auRXf+ z5+y9mmb*?8_U@xMKhu}|N+uF!i(jV*3ll*k$bp=wkSU`qISg?ELQ@knnGzF2#Y_oK zK_vRH|M=+BeaZKKArfX^;?u+(4Rp?Kj6E(Tgo?4caV$+f#Cx&WOuVc)OUG@Dk5Wa9 z%ispu5Elh8)4CO{mVuz9tYy0Q=DYV_>`VUo)kv7Fu9rynN;{e$=kU=|RG!_*7jtt* za>{Xb=&7>pX$oj}))~ang%q$u*hb4#HSy@qUAj7j*=*e@RR#Yo-hXuGj=OAz*xcPo z!mf?H5J^5C3A2^_O<+X$r;$&Q@j{3l<#hZH9#4D{GO}NegxOl&DaOO-+c^tY-dNE0 zXVa7i`))a}dLUes4Da&AAlW8#TZ z)<+23;bANUF(KtJ_vvss0R>6`0V!kwQz2N22b&Pndd=IKu~q%J6W9bmo0{t$JP>6w zKkb4f2XLVR;5l$Aw++}1d`}+7I7KFn0Neregt03{prr_h`0rIPoa;Tzz+E3c)&hu1 zt$#SBV`^-1Au+v>jKVz=wIk0$)~Cx%3FBOe$!2yjfpLZ`_gkO}&JQmX9h1O1w4;SW zn?AZkWHrJ}IR|(j7l)Z?$gcB4Wnip`&2sk0pCmF2t{=#gM;DpWQK^h`Bg}Lj{zsPg zHYQccSoFgIw<${p=E)YwpcZ8lWbEGrPp?iId3p(FZ8~ZE2=nTziv^qq zK~oS6TbZ&`eD&4Wn04>{AUG=QnqRwYFF0jffZG%9ufp{u&OzzHFq43}kHQm}`}4?z zpJB2PTDFTpS46(gGg@samY4HF1GW6*sC*PSAO$quNZ5prNXDiU?@ zUt=R08;t=nu>Zr~{yoquFe|LI$fYL1X^q zx84No_75^$?AEZ7Bc2pn{WiQki3Vl`qvC&_cUX0v9^W7!|lfh(S4~ zk|%c-5tRt%A?h7VjfVhNLL32kki%rkzQI3f?gZwePRC3;8NmAP#MfFXhoHv0KyM8i zW_&9mCPh(rYVzKKK~Z&oP4Kx}=dR2s}+5VcgH7A3}9qxRoJ+qtbB~)C$&MWy`_EfyAxG zNajSt;B8VPhIl?#fO`eKqAlhn8-n*1%!3OzQAzsQLk3WnVujwk2_d>COJo7G0ZS3|MF5ypIF;gq zG=VCP1T^&b?{?A9E*jcJLu)}zA1vvjp(M=z z$k5O*fyEdulIG9s(LKgnTuEkRL(J_Z83z~&33eG^F&8Co4cfb!hDZf+7WgoNvbeaf zw_MbrzwXte5Z^#}vWI2nclw#8HzSueS`Pm2}X6e|Nj zzC?gPfCF)fR3K1+B#MAq0p>^wH*hPt1y~z=Lm;mvrr%60x~=}$^VUoOS@&KKvr5vgX*}*TDS-q-ew-Q!Mar!Nct=% zPtddsRD8%<3xzH>4sy#f6Yz7po zK8!xG9>N7-(&w0{We&<*RnB1k_lfpX-LNqDlPpX)!GCGp6(%4J)^$XTlFA6w)YYC$ zxhYm@b%*5tS0b^V{=1P_?>E9_|J_k-mcYB(7oZ9Cl}MPobvhNIhcS$E;1VttlPx}b ztR0u~D7Iu&3=}KN<|W?x7{pO~u#$7PKGx3DX{*3;bTkoV<`F+CTJ zGpo~yDgPDHy%1eU#7CH!crw9^x$l`yFq;*D<3(m+ky%PcH&NZFq!~}*_CXyU&iaSA%R?YfUnvzbQ14;Bw-qj8A}edzSPBL zPywcmLbP!QBA6=Z<0@U4OAi05=fbhKxoA9dLPkvBHQo>{uT&R%7GF!1|;o?Mb-x=f^dP8~^eL_8_?NUkz8{d3&(vWNBUC z6s3t}F@k!VyCE0yFIuU)(9uk_bK=`rV_(HN5y&{UW3m02Wr05~3PO5qDnFZ&%17;w zKtQds&KB(;YJ)cqVyoreyn&UE%&GhRIt}z}cf_-k1TN=5|7gZ7A%8R(IjL3y907?S zS*A!O$4vyQ4i0M9G$SS{qUS4IuW|&~id#)MJ!fwHV$k4Y?~(MFxOKX-`m{QZTVw(` zL(!V1xTI$nSf%g?aP9e!0`zWX%t{skbae(k&<=lI->!S&(96XmiUB!HDnnX{Px5|% zsB$EU2af7z3)*9WaO(R*2!t$9l=muLP*c@vHvd;d+W&v$)gJg8{O8*V$Vva=&2CQG z($gl|uo$!}C;i?mD8Pr)T?P2KDnO+SIm8Y@CF z42Eh9`14xvy#p4=MIeVlDs?4uj|q5*os+HS5h5fM?Qm{c~Y=)`N#umLW&o3 zQe=;dx9U>oz@3vpcGdE%g%G(#O(|@`5=eWH4Yh}MYBnvQJ;+enLp!yNme3w#744y& z8bVmwFWuUvZKw&^N*?C-jvqc$%Ln>_w8l>qtPXh@|VL`#EPbz?km&N3yzf2 zkhPRtK3sLw!qlR*T$~d+#D#hW7jSx50gg2~0g-)Ysh-Zp`tE94I z*@>mY9i>BTXlJl7PqvE|q25e7L#oS`b~+DEd8(L@W{+p5rHA7iJNim=F19z7oaqn` z`WZYVp$`;#LxBYWt{Pi`g#RW<`k%0w_V7T>q$NBA z8A^L-r?$}&+Jg+FJ+xDs2uu5=Tfvr!%gMqVM)Fi?Vt#MjlqYtU=cC1y4mlj2JHz8t zoqxUCgz={)WwFAtK6NNYW7^c(Zgf=6r_z%cPtSLX4gdJ@#`GnQp>k7x!fEpS>1?c{ zmE)<{k-b_(lj$RCH9fXF1DBC)bc&6$lI5w4WkHK%4pJ7Bf@ViAYvIpGk)@42Zf!Li z%TC8LJE?qS#?oaouC?uYK9IVbQ+Zc#o|Pjf^W>;c*k4<>6!U&Y+(hyfWH#+_L$0NS z^+SPcSdg)_iTI}aZUTu9GLbeB-@3dv$S~SOd{c?Z#e)M&GY@n8Eb7#BoRtuJX??@$J?-m zZ6dyPxo?nNw2AnplHSg)UbJ7=FZ!Q*cxCHmp$38IA`j=vy^B1wrCGfNb>W~CifpMP z1y{S`bqfsFe1E}p?3KS%Bwmt8(rj~x``!l^$N0@Hu=yl@?es-#UKl5ljK{M M^FEEp1I_>c52y2}fdBvi literal 176128 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 diff --git a/tools/files_tool.py b/tools/files_tool.py index 8e508e7e..8209ed9f 100644 --- a/tools/files_tool.py +++ b/tools/files_tool.py @@ -1,7 +1,7 @@ import os import asyncio from typing import Dict, Any -from agentpress.tool import Tool, ToolResult +from agentpress.tool import Tool, ToolResult, tool_schema from agentpress.config import settings class FilesTool(Tool): @@ -10,6 +10,18 @@ class FilesTool(Tool): self.workspace = settings.workspace_dir os.makedirs(self.workspace, exist_ok=True) + @tool_schema({ + "name": "create_file", + "description": "Create a new file in the workspace", + "parameters": { + "type": "object", + "properties": { + "file_path": {"type": "string", "description": "The relative path of the file to create"}, + "content": {"type": "string", "description": "The content to write to the file"} + }, + "required": ["file_path", "content"] + } + }) async def create_file(self, file_path: str, content: str) -> ToolResult: try: full_path = os.path.join(self.workspace, file_path) @@ -22,6 +34,17 @@ class FilesTool(Tool): except Exception as e: return self.fail_response(f"Error creating file: {str(e)}") + @tool_schema({ + "name": "read_file", + "description": "Read the contents of a file in the workspace", + "parameters": { + "type": "object", + "properties": { + "file_path": {"type": "string", "description": "The relative path of the file to read"} + }, + "required": ["file_path"] + } + }) async def read_file(self, file_path: str) -> ToolResult: try: full_path = os.path.join(self.workspace, file_path) @@ -31,6 +54,18 @@ class FilesTool(Tool): except Exception as e: return self.fail_response(f"Error reading file: {str(e)}") + @tool_schema({ + "name": "update_file", + "description": "Update the contents of a file in the workspace", + "parameters": { + "type": "object", + "properties": { + "file_path": {"type": "string", "description": "The relative path of the file to update"}, + "content": {"type": "string", "description": "The new content to write to the file"} + }, + "required": ["file_path", "content"] + } + }) async def update_file(self, file_path: str, content: str) -> ToolResult: try: full_path = os.path.join(self.workspace, file_path) @@ -40,6 +75,18 @@ class FilesTool(Tool): 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", + "parameters": { + "type": "object", + "properties": { + "file_path": {"type": "string", "description": "The relative path of the file to delete"} + }, + "required": ["file_path"] + } + }) async def delete_file(self, file_path: str) -> ToolResult: try: full_path = os.path.join(self.workspace, file_path) @@ -48,74 +95,6 @@ class FilesTool(Tool): except Exception as e: return self.fail_response(f"Error deleting file: {str(e)}") - def get_schemas(self) -> Dict[str, Dict[str, Any]]: - schemas = { - "create_file": { - "name": "create_file", - "description": "Create a new file in the workspace", - "parameters": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "The relative path of the file to create" - }, - "content": { - "type": "string", - "description": "The content to write to the file" - } - }, - "required": ["file_path", "content"] - } - }, - "read_file": { - "name": "read_file", - "description": "Read the contents of a file in the workspace", - "parameters": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "The relative path of the file to read" - } - }, - "required": ["file_path"] - } - }, - "update_file": { - "name": "update_file", - "description": "Update the contents of a file in the workspace", - "parameters": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "The relative path of the file to update" - }, - "content": { - "type": "string", - "description": "The new content to write to the file" - } - }, - "required": ["file_path", "content"] - } - }, - "delete_file": { - "name": "delete_file", - "description": "Delete a file from the workspace", - "parameters": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "The relative path of the file to delete" - } - }, - "required": ["file_path"] - } - } - } - return {name: self.format_schema(schema) for name, schema in schemas.items()} if __name__ == "__main__": async def test_files_tool(): @@ -150,4 +129,4 @@ if __name__ == "__main__": read_deleted_result = await files_tool.read_file(test_file_path) print("Read deleted file result:", read_deleted_result) - asyncio.run(test_files_tool()) \ No newline at end of file + asyncio.run(test_files_tool()) diff --git a/tools/tool_example.py b/tools/tool_example.py deleted file mode 100644 index dbf0e746..00000000 --- a/tools/tool_example.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Dict, Any -from agentpress.tool import Tool, ToolResult - -class ExampleTool(Tool): - description = "An example tool for demonstration purposes." - - def __init__(self): - super().__init__() - - async def example_function(self, input_text: str) -> ToolResult: - try: - processed_text = input_text.upper() - return self.success_response({ - "original_text": input_text, - "processed_text": processed_text - }) - except Exception as e: - return self.fail_response(f"Error processing input: {str(e)}") - - def get_schemas(self) -> Dict[str, Dict[str, Any]]: - schemas = { - "example_function": { - "name": "example_function", - "description": "An example function that demonstrates the usage of the Tool class", - "parameters": { - "type": "object", - "properties": { - "input_text": { - "type": "string", - "description": "The text to be processed by the example function" - } - }, - "required": ["input_text"] - } - } - } - return {name: self.format_schema(schema) for name, schema in schemas.items()} \ No newline at end of file diff --git a/workspace/AI_Discussion_Summary.txt b/workspace/AI_Discussion_Summary.txt deleted file mode 100644 index 9afaf6d0..00000000 --- a/workspace/AI_Discussion_Summary.txt +++ /dev/null @@ -1,5 +0,0 @@ -Artificial Intelligence (AI) is a branch of computer science that aims to create systems capable of performing tasks that would typically require human intelligence. These tasks include learning, reasoning, problem-solving, perception, and language understanding. - -AI can be categorized into Narrow AI, designed for a specific task like facial recognition, and General AI, which should be able to perform any intellectual task a human can do. - -AI has a broad range of applications, from healthcare (where it can predict disease outbreaks) to finance (where can be used to detect fraudulent transactions). \ No newline at end of file diff --git a/workspace/app.py b/workspace/app.py deleted file mode 100644 index aa766726..00000000 --- a/workspace/app.py +++ /dev/null @@ -1,14 +0,0 @@ -from flask import Flask, render_template - -app = Flask(__name__) - -@app.route('/') -def home(): - return render_template('index.html') - -@app.route('/about') -def about(): - return render_template('about.html') - -if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file diff --git a/workspace/main.py b/workspace/main.py deleted file mode 100644 index 54decf04..00000000 --- a/workspace/main.py +++ /dev/null @@ -1,10 +0,0 @@ -from flask import Flask, render_template - -app = Flask(__name__) - -@app.route('/') -def home(): - return render_template('index.html') - -if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file