├── .gitignore ├── Agently ├── Agent │ ├── Agent.py │ ├── AgentFactory.py │ └── __init__.py ├── AppConnector │ ├── AppConnector.py │ ├── __init__.py │ └── builtins │ │ ├── __init__.py │ │ ├── gradio.py │ │ ├── shell.py │ │ └── streamlit.py ├── Facility │ ├── FacilityManager.py │ └── __init__.py ├── FastServer │ ├── FastServer.py │ ├── __init__.py │ └── builtins │ │ ├── __init__.py │ │ └── mcp.py ├── LICENSE ├── Request │ ├── Request.py │ └── __init__.py ├── Stage │ ├── EventEmitter.py │ ├── MessageCenter.py │ ├── Stage.py │ ├── StageFunction.py │ ├── StageHybridGenerator.py │ ├── StageResponse.py │ ├── Tunnel.py │ └── __init__.py ├── Workflow │ ├── Chunk │ │ ├── Condition.py │ │ ├── Loop.py │ │ ├── __init__.py │ │ └── helper.py │ ├── MainExecutor.py │ ├── Runtime │ │ ├── BranchState.py │ │ ├── Checkpoint.py │ │ ├── Repository.py │ │ ├── Snapshot.py │ │ ├── State.py │ │ └── __init__.py │ ├── Schema.py │ ├── Workflow.py │ ├── __init__.py │ ├── executors │ │ ├── __init__.py │ │ ├── builtin │ │ │ ├── ConditionExecutor.py │ │ │ ├── EndExecutor.py │ │ │ ├── StartExecutor.py │ │ │ ├── __init__.py │ │ │ └── install.py │ │ └── generater │ │ │ ├── __init__.py │ │ │ └── loop.py │ ├── lib │ │ ├── BreakingHub.py │ │ ├── ChunkExecutorManager.py │ │ ├── Store.py │ │ ├── __init__.py │ │ ├── constants.py │ │ └── painter.py │ ├── types │ │ ├── __init__.py │ │ └── chunk_data.py │ ├── utils │ │ ├── __init__.py │ │ ├── chunk_helper.py │ │ ├── exec_tree.py │ │ ├── find.py │ │ ├── logger.py │ │ ├── runner.py │ │ ├── runtime_supports.py │ │ └── verify.py │ └── yamlflow │ │ ├── __init__.py │ │ ├── preset_chunks │ │ ├── Print.py │ │ ├── Start.py │ │ ├── UserInput.py │ │ └── __init__.py │ │ └── yamlflow.py ├── __init__.py ├── _global.py ├── global_plugin_manager.py ├── plugins │ ├── __init__.py │ ├── agent_component │ │ ├── Decorator.py │ │ ├── EventListener.py │ │ ├── Instant.py │ │ ├── OpenAIAssistant.py │ │ ├── ReplyReformer.py │ │ ├── ResponseGenerator.py │ │ ├── Role.py │ │ ├── Search.py │ │ ├── Segment.py │ │ ├── Session.py │ │ ├── Status.py │ │ ├── Tool.py │ │ ├── UserInfo.py │ │ ├── YAMLLoader.py │ │ ├── __init__.py │ │ ├── config.ini │ │ └── utils │ │ │ ├── ComponentABC.py │ │ │ └── __init__.py │ ├── facility │ │ ├── Embedding.py │ │ ├── RoleManager.py │ │ ├── StatusManager.py │ │ ├── __init__.py │ │ ├── config.ini │ │ └── utils │ │ │ ├── FacilityABC.py │ │ │ └── __init__.py │ ├── request │ │ ├── AzureOpenAI.py │ │ ├── Bedrock.py │ │ ├── Claude.py │ │ ├── ERNIE.py │ │ ├── Google.py │ │ ├── Hunyuan.py │ │ ├── LiteLLM.py │ │ ├── MiniMax.py │ │ ├── OAIClient.py │ │ ├── OpenAI.py │ │ ├── PerfXCloud.py │ │ ├── QianFan.py │ │ ├── __init__.py │ │ ├── config.ini │ │ ├── generate_ENIRE_access_token.sh │ │ └── utils │ │ │ ├── RequestABC.py │ │ │ ├── __init__.py │ │ │ ├── format.py │ │ │ └── transform.py │ ├── storage │ │ ├── FileStorage.py │ │ ├── SQLite.py │ │ ├── __init__.py │ │ ├── config.ini │ │ └── utils │ │ │ ├── StorageABC.py │ │ │ └── __init__.py │ └── tool │ │ ├── Code.py │ │ ├── Web.py │ │ ├── __init__.py │ │ ├── config.ini │ │ └── utils │ │ ├── ToolABC.py │ │ └── __init__.py ├── requirements.txt └── utils │ ├── AliasManager.py │ ├── DataGenerator.py │ ├── DataOps.py │ ├── IdGenerator.py │ ├── MCPClient.py │ ├── PluginManager.py │ ├── RuntimeCtx.py │ ├── StorageDelegate.py │ ├── ToolManager.py │ ├── __init__.py │ ├── check_version.py │ ├── lexer │ ├── LICENSE │ ├── __init__.py │ ├── lexer.py │ ├── lexer_helper.py │ ├── lexer_tokens.py │ └── pyproject.toml │ ├── load_json.py │ └── transform.py ├── LICENSE ├── README.md ├── docs ├── Why-does-AI-application-development-need-a-good-development-framework.md └── guidebook │ ├── Agently_AI应用开发框架由浅入深的应用开发指导_应用开发者入门篇.pdf │ ├── Agently_step_by_step_guide-simple.pdf │ ├── Agently_step_by_step_guide.ipynb │ ├── application_development_handbook.ipynb │ └── introduction.ipynb ├── examples ├── deepseek_reasoning.ipynb ├── mcp │ ├── agent_as_mcp_server.py │ ├── mcp_server_example.py │ ├── mcp_tool_using.py │ └── workflow_as_mcp_server.py └── planning_loop_demo.py ├── playground ├── NPC_in_game_generate_choices_using_google_gemini.ipynb ├── README.md ├── SQL_generator.py ├── character_change_behaviours_according_mood_status.ipynb ├── concurrency_and_asynchornous_dependency.ipynb ├── contrast_between_Agently_workflow_and_LangGraph.ipynb ├── create_event_listeners_with_alias_or_decorator.ipynb ├── finding_connectable_pairs_from_text_tailers_and_headers.ipynb ├── function_calling.py ├── generate_agent_powered_function_in_runtime_using_decorator.ipynb ├── human_step_in_before_reply.ipynb ├── long_text_to_qa_pairs.ipynb ├── mimir.py ├── open_source_navigator_agent-Wang_Yafang.ipynb ├── planner_workflow.py ├── predict_data_according_given_data_set.ipynb ├── routing_to_different_agent_group_for_users_with_different_authorities.ipynb ├── simple_role_play.py ├── sql_generator.ipynb ├── story_maker.py ├── stream_output_in_streamlit.py ├── survey_agent_asks_questions_according_form.ipynb ├── teacher_with_search_ability_for_kids.ipynb ├── test_run_template.ipynb ├── using_local_open_source_model_to_drive_agents.ipynb ├── using_tools_to_enhance_your_agent.ipynb ├── web_socket.py ├── workflow.py ├── workflow_gernerate_code.py ├── workflow_series_01_building_a_multi_round_chat.ipynb ├── workflow_series_02_using_condition_to_branch_off.ipynb ├── workflow_series_03_using_decorator_to_create_chunks.ipynb ├── workflow_series_04_draw_a_workflow_graph.ipynb ├── workflow_series_05_seperating_generation_steps_and_reply_in_every_step.ipynb ├── writing_ad_copies_according_image.ipynb └── 线上快速试用模板.ipynb ├── poetry.lock └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .DS_Store 3 | .AppleDouble 4 | .Trashes 5 | 6 | *.o 7 | *.a 8 | *.exe 9 | *.pyc 10 | 11 | *.log 12 | *.bak 13 | *.swp 14 | 15 | *.tmp 16 | *.temp 17 | 18 | node_modules 19 | .cache 20 | .npmignore 21 | __pycache__ 22 | 23 | .download 24 | .upload 25 | 26 | test.py 27 | test.js 28 | private_settings 29 | myenv 30 | *test* 31 | *_old* 32 | *.old 33 | 34 | .idea/ 35 | dist -------------------------------------------------------------------------------- /Agently/Agent/AgentFactory.py: -------------------------------------------------------------------------------- 1 | from ..utils import PluginManager, ToolManager, RuntimeCtx, RuntimeCtxNamespace 2 | from .._global import global_plugin_manager, global_storage, global_settings, global_tool_manager 3 | from .Agent import Agent 4 | 5 | class AgentFactory(object): 6 | def __init__( 7 | self, 8 | *, 9 | parent_plugin_manager: object=global_plugin_manager, 10 | parent_tool_manager: object=global_tool_manager, 11 | parent_settings: object=global_settings, 12 | is_debug: bool=False 13 | ): 14 | #runtime ctx 15 | self.factory_agent_runtime_ctx = RuntimeCtx() 16 | self.settings = RuntimeCtx(parent = parent_settings) 17 | 18 | #use plugin manager 19 | self.plugin_manager = PluginManager(parent = parent_plugin_manager) 20 | 21 | #use tool manager 22 | self.tool_manager = ToolManager(parent = parent_tool_manager) 23 | 24 | #use global storage 25 | self.global_storage = global_storage 26 | 27 | #debug 28 | self.set_settings("is_debug", is_debug) 29 | 30 | def create_agent(self, agent_id: str=None, is_debug: bool=False): 31 | return Agent( 32 | agent_id = agent_id, 33 | parent_agent_runtime_ctx = self.factory_agent_runtime_ctx, 34 | parent_tool_manager = self.tool_manager, 35 | global_storage = self.global_storage, 36 | parent_plugin_manager = self.plugin_manager, 37 | parent_settings = self.settings, 38 | is_debug = is_debug 39 | ) 40 | 41 | def register_plugin(self, module_name: str, plugin_name: str, plugin: callable): 42 | self.plugin_manager.register(module_name, plugin_name, plugin) 43 | return self 44 | 45 | def attach_workflow(self, name: str, workflow: object): 46 | class AttachedWorkflow: 47 | def __init__(self, agent: object): 48 | self.agent = agent 49 | self.get_debug_status = lambda: self.agent.settings.get_trace_back("is_debug") 50 | self.settings = RuntimeCtxNamespace(f"plugin_settings.agent_component.{ name }", self.agent.settings) 51 | 52 | def start_workflow(self, init_inputs: dict=None, init_storage: dict={}): 53 | if not isinstance(init_storage, dict): 54 | raise Exception("[Workflow] Initial storage must be a dict.") 55 | init_storage.update({ "$agent": self.agent }) 56 | return workflow.start(init_inputs, storage=init_storage) 57 | 58 | def export(self): 59 | return { 60 | "alias": { 61 | name: { "func": self.start_workflow, "return_value": True }, 62 | } 63 | } 64 | return self.register_plugin("agent_component", name, AttachedWorkflow) 65 | 66 | def set_settings(self, settings_key: str, settings_value: any): 67 | self.settings.set(settings_key, settings_value) 68 | return self 69 | 70 | def set_global_variable(self, variable_name: str, variable_value: any): 71 | self.settings.set(f"global_variables.{ variable_name }", variable_value) 72 | return self 73 | 74 | def toggle_component(self, component_name: str, is_enabled: bool): 75 | self.set_settings(f"component_toggles.{ component_name }", is_enabled) 76 | return self 77 | 78 | def set_proxy(self, proxy_setting: any): 79 | self.set_settings("proxy", proxy_setting) 80 | return self -------------------------------------------------------------------------------- /Agently/Agent/__init__.py: -------------------------------------------------------------------------------- 1 | from .AgentFactory import AgentFactory 2 | from .Agent import Agent -------------------------------------------------------------------------------- /Agently/AppConnector/AppConnector.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from ..utils import DataGenerator, DataOps 3 | from .builtins import gradio_app, streamlit_app, shell_app 4 | 5 | class AppConnector(object): 6 | def __init__( 7 | self, 8 | *, 9 | app_name:str="gradio", 10 | message_handler:callable=None, 11 | binded_agent:object=None, 12 | ): 13 | self.app_name = app_name 14 | self.message_handler = message_handler 15 | self.message_handler_thread = None 16 | self.binded_agent = binded_agent 17 | self.buffer = "" 18 | self.app_dict = { 19 | "gradio": gradio_app, 20 | "streamlit": streamlit_app, 21 | "shell": shell_app, 22 | } 23 | self.runtime = DataOps() 24 | self.data_generator = DataGenerator() 25 | 26 | def register_app(self, type_name:str, app:callable): 27 | self.app_dict.update({ type_name: app }) 28 | return self 29 | 30 | def use_app(self, app_name:str): 31 | self.app_name = app_name 32 | return self 33 | 34 | def set_message_handler(self, message_handler: callable): 35 | self.message_handler = message_handler 36 | return self 37 | 38 | def run_message_handler(self, *args, **kwargs): 39 | self.message_handler_thread = threading.Thread( 40 | target=self.message_handler, 41 | args=args, 42 | kwargs=kwargs, 43 | ) 44 | self.message_handler_thread.start() 45 | return self 46 | 47 | def bind_agent(self, agent): 48 | self.binded_agent = agent 49 | if self.message_handler == None: 50 | self.set_message_handler( 51 | lambda message, chat_history: agent.chat_history(chat_history).input(message).start() 52 | ) 53 | return self 54 | 55 | def refresh_binded_agent(self): 56 | if self.binded_agent: 57 | @self.binded_agent.on_event("delta") 58 | def delta_handler(data): 59 | self.emit_delta(data) 60 | @self.binded_agent.on_event("done") 61 | def done_handler(data): 62 | self.emit_done(data) 63 | return self 64 | 65 | def emit(self, event: str, data: any): 66 | self.data_generator.event.emit(event, data) 67 | return self 68 | 69 | def emit_delta(self, delta:str=None): 70 | self.buffer += delta if delta else "" 71 | self.emit("delta", delta) 72 | self.emit("buffer", self.buffer) 73 | return self 74 | 75 | def emit_buffer(self, buffer:str=None): 76 | self.buffer = buffer if buffer else "" 77 | self.emit("buffer", self.buffer) 78 | return self 79 | 80 | def emit_done(self, final_result:str=None): 81 | self.emit("done", final_result if final_result else self.buffer) 82 | self.buffer = "" 83 | return self 84 | 85 | def on(self, event:str, handler:callable): 86 | self.data_generator.event.on(event, handler) 87 | return self 88 | 89 | def reset_data_generator(self): 90 | self.data_generator = DataGenerator() 91 | return self 92 | 93 | def run(self, app_name:str=None, **kwargs): 94 | app_name = app_name or self.app_name 95 | self.app_dict[app_name](self, **kwargs) 96 | if self.message_handler_thread: 97 | self.message_handler_thread.join() 98 | self.message_handler_thread = None -------------------------------------------------------------------------------- /Agently/AppConnector/__init__.py: -------------------------------------------------------------------------------- 1 | from .AppConnector import AppConnector -------------------------------------------------------------------------------- /Agently/AppConnector/builtins/__init__.py: -------------------------------------------------------------------------------- 1 | from .gradio import gradio_app 2 | from .streamlit import streamlit_app 3 | from .shell import shell_app -------------------------------------------------------------------------------- /Agently/AppConnector/builtins/gradio.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | def gradio_app(app_connector, **kwargs): 4 | """ 5 | Support Kwargs: 6 | multi_round (bool=True): Turn on or turn off multi round chat. 7 | interface (dict): Additional interface options dict. 8 | launch (dict): Additional launch options dict. 9 | """ 10 | is_multi_round = kwargs.get("multi_round", True) 11 | is_fluency = kwargs.get("fluency", False) 12 | try: 13 | import gradio as gr 14 | except: 15 | raise Exception("[Application Connector] Package 'gradio' require to be installed. Use 'pip install gradio' to install.") 16 | def app_executor(message, history, *args): 17 | if app_connector.message_handler: 18 | # Refresh binded agent event 19 | app_connector.refresh_binded_agent() 20 | # Format chat history 21 | chat_history = [] 22 | if is_multi_round: 23 | for round in history: 24 | if round[0]: 25 | chat_history.append({ "role": "user", "content": round[0] }) 26 | if round[1]: 27 | chat_history.append({ "role": "assistant", "content": round[1] }) 28 | # Define generator event handlers 29 | app_connector.on("buffer", lambda data: { "yield": data }) 30 | app_connector.on("done", lambda data: { "yield": data, "end": True }) 31 | # Run message handler 32 | app_connector.run_message_handler(message, chat_history, *args) 33 | # Get response from data generator 34 | try: 35 | for item in app_connector.data_generator.start(): 36 | if is_fluency: 37 | time.sleep(0.01) 38 | if item: 39 | yield item 40 | app_connector.reset_data_generator() 41 | return 42 | except StopIteration: 43 | app_connector.reset_data_generator() 44 | return 45 | else: 46 | return "No Message Handler, use `.set_message_handler()` to set one." 47 | gr.ChatInterface( 48 | app_executor, 49 | title="Agently Gradio Chatbot", 50 | description="A Chatbot powered by Agently & Gradio", 51 | textbox=gr.Textbox(placeholder="Now let's talk..."), 52 | **(kwargs.get("interface", {})) 53 | ).launch(**(kwargs.get("launch", {}))) -------------------------------------------------------------------------------- /Agently/AppConnector/builtins/shell.py: -------------------------------------------------------------------------------- 1 | def shell_app(app_connector, **kwargs): 2 | """ 3 | Support Kwargs: 4 | user_name (str): Set user name to be displayed. 5 | assistant_name (dict): Set assistant name to be displayed. 6 | """ 7 | if not app_connector.message_handler: 8 | raise Exception("No Message Handler, use `.set_message_handler()` to set one.") 9 | user_name = kwargs.get("user_name", "User") 10 | assistant_name = kwargs.get("assistant_name", "Assistant") 11 | message = "" 12 | chat_history = [] 13 | while True: 14 | message = input(f"[{ user_name }]:") 15 | if message == "#exit": 16 | break 17 | # Define generator event handlers 18 | app_connector.on("delta", lambda data: { "yield": data }) 19 | app_connector.on("done", lambda data: { "end": True }) 20 | # Run message handler 21 | app_connector.run_message_handler(message, chat_history) 22 | print(f"[{ assistant_name }]:") 23 | buffer = "" 24 | try: 25 | for item in app_connector.data_generator.start(): 26 | if item: 27 | print(item, end="") 28 | buffer += item 29 | chat_history.append({ "role": "user", "content": message }) 30 | chat_history.append({ "role": "assistant", "content": buffer }) 31 | app_connector.reset_data_generator() 32 | continue 33 | except StopIteration: 34 | app_connector.reset_data_generator() 35 | continue -------------------------------------------------------------------------------- /Agently/AppConnector/builtins/streamlit.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import subprocess 4 | 5 | def streamlit_app(app_connector, **kwargs): 6 | is_multi_round = kwargs.get("multi_round", True) 7 | is_fluency = kwargs.get("fluency", False) 8 | st = app_connector.runtime.get("package.streamlit") 9 | if not st: 10 | try: 11 | import streamlit as st 12 | app_connector.runtime.set("package.streamlit", st) 13 | except: 14 | raise Exception("[Application Connector] Package 'streamlit' require to be installed. Use 'pip install streamlit' to install.") 15 | def get_chat_response(): 16 | for item in app_connector.data_generator.start(): 17 | if is_fluency: 18 | for char in list(item): 19 | time.sleep(0.01) 20 | yield char 21 | else: 22 | yield item 23 | st.title("A Chatbot powered by Agently & Streamlit") 24 | if "chat_history" not in st.session_state: 25 | st.session_state.chat_history = [] 26 | for message in st.session_state.chat_history: 27 | with st.chat_message(message["role"]): 28 | st.markdown(message["content"]) 29 | message = st.chat_input("Now let's talk...") 30 | if message: 31 | if not app_connector.message_handler: 32 | st.warning("No Message Handler, use `.set_message_handler()` to set one.", icon="⚠️") 33 | 34 | # Refresh binded agent event 35 | app_connector.refresh_binded_agent() 36 | # Define generator event handlers 37 | app_connector.on("delta", lambda data: { "yield": data }) 38 | app_connector.on("done", lambda data: { "end": True }) 39 | with st.chat_message("user"): 40 | st.markdown(message) 41 | st.session_state.chat_history.append({"role": "user", "content": message}) 42 | with st.spinner("Processing..."): 43 | with st.chat_message("assisitant"): 44 | if is_multi_round: 45 | app_connector.run_message_handler( 46 | message, 47 | st.session_state.chat_history[:-1] 48 | if len(st.session_state.chat_history) > 0 49 | else [] 50 | ) 51 | else: 52 | app_connector.run_message_handler(message, []) 53 | response_generator = get_chat_response() 54 | response = st.write_stream(response_generator) 55 | st.session_state.chat_history.append({ "role": "assistant", "content": response }) 56 | app_connector.reset_data_generator() -------------------------------------------------------------------------------- /Agently/Facility/FacilityManager.py: -------------------------------------------------------------------------------- 1 | from ..utils import PluginManager, RuntimeCtx, RuntimeCtxNamespace 2 | from .._global import global_plugin_manager, global_storage, global_settings 3 | 4 | class FacilityManager(object): 5 | def __init__(self, *, storage: object=global_storage, parent_plugin_manager: object=global_plugin_manager, parent_settings: object = global_settings): 6 | # init plugin manager 7 | self.plugin_manager = PluginManager(parent = parent_plugin_manager) 8 | # use global storage 9 | self.storage = storage 10 | # init facility settings 11 | self.settings = RuntimeCtx(parent = global_settings) 12 | # install facilities 13 | self.refresh_plugins() 14 | 15 | def refresh_plugins(self): 16 | facilities = self.plugin_manager.get("facility") 17 | for facility_name, FacilityPluginClass in facilities.items(): 18 | setattr(self, facility_name, FacilityPluginClass(storage = self.storage, plugin_manager = self.plugin_manager, settings = self.settings)) 19 | 20 | def set_settings(self, settings_key: str, settings_value: any): 21 | self.settings.set(settings_key, settings_value) 22 | return self 23 | 24 | def list(self): 25 | result = {} 26 | facility_names = [\ 27 | attr_name for attr_name in dir(self)\ 28 | if not attr_name.startswith("_")\ 29 | and attr_name not in ("plugin_manager", "storage", "list")\ 30 | and isinstance(getattr(self, attr_name), object)\ 31 | ] 32 | for facility_name in facility_names: 33 | facility = getattr(self, facility_name) 34 | facility_method_names = [\ 35 | method_name for method_name in dir(facility)\ 36 | if not method_name.startswith("_")\ 37 | and callable(getattr(facility, method_name))\ 38 | ] 39 | result[facility_name] = facility_method_names 40 | return result -------------------------------------------------------------------------------- /Agently/Facility/__init__.py: -------------------------------------------------------------------------------- 1 | from .FacilityManager import FacilityManager -------------------------------------------------------------------------------- /Agently/FastServer/FastServer.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Literal 2 | from Agently.Agent import Agent 3 | from Agently.Workflow import Workflow 4 | 5 | class FastServer: 6 | #def __init__(self, type: Literal["mcp", "websocket", "sse", "http"]): 7 | def __init__(self, type: Literal["mcp"]): 8 | if type == "mcp": 9 | from .builtins import MCPServerHandler as ServerHandler 10 | elif type == "websocket": 11 | from .builtins import WebSocketServerHandler as ServerHandler 12 | elif type == "sse": 13 | from .builtins import SSEServerHandler as ServerHandler 14 | elif type == "http": 15 | from .builtins import HTTPServerHandler as ServerHandler 16 | else: 17 | raise TypeError(f"[Agently FastServer] Can not support type '{ type }.'") 18 | self._server_handler = ServerHandler() 19 | self.serve = self._server_handler.serve -------------------------------------------------------------------------------- /Agently/FastServer/__init__.py: -------------------------------------------------------------------------------- 1 | from .FastServer import FastServer -------------------------------------------------------------------------------- /Agently/FastServer/builtins/__init__.py: -------------------------------------------------------------------------------- 1 | from .mcp import MCPServerHandler -------------------------------------------------------------------------------- /Agently/FastServer/builtins/mcp.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from Agently.Agent import Agent 3 | from Agently.Workflow import Workflow 4 | from mcp.server import FastMCP 5 | 6 | class MCPServerHandler: 7 | def serve_agent(self, agent: Agent, *, name:str=None, desc:str=None): 8 | if name is None or name == "": 9 | name = "Agently Agent" 10 | if desc is None or desc == "": 11 | desc = "Ask this agent anything for help." 12 | 13 | server = FastMCP(name) 14 | 15 | async def ask_agent(message:str): 16 | return agent.input(message).start() 17 | server.tool( 18 | name=name, 19 | description=desc, 20 | )(ask_agent) 21 | 22 | server.run() 23 | 24 | def serve_workflow(self, workflow: Workflow, *, name:str=None, desc:str=None): 25 | if name is None or name == "": 26 | name = "Agently Workflow" 27 | if desc is None or desc == "": 28 | desc = "A helpful workflow to solve any problem you submit." 29 | 30 | server = FastMCP(name) 31 | 32 | async def submit_to_workflow(message:str, initial_storage:dict=None): 33 | result = workflow.start(message, storage=initial_storage) 34 | return result["default"] 35 | submit_to_workflow.__doc__ = desc 36 | server.tool()(submit_to_workflow) 37 | 38 | server.run() 39 | 40 | def serve(self, target: Union[Agent, Workflow], *, name:str=None, desc:str=None): 41 | if isinstance(target, Agent): 42 | self.serve_agent(target, name=name, desc=desc) 43 | elif isinstance(target, Workflow): 44 | self.serve_workflow(target, name=name, desc=desc) 45 | else: 46 | raise TypeError(f"[Agently FastServer] Can not support target '{ target }'. Server target must be an Agently Agent or a Agently Workflow.") -------------------------------------------------------------------------------- /Agently/Request/__init__.py: -------------------------------------------------------------------------------- 1 | from .Request import Request -------------------------------------------------------------------------------- /Agently/Stage/EventEmitter.py: -------------------------------------------------------------------------------- 1 | from .Stage import Stage 2 | 3 | class EventEmitter: 4 | def __init__(self, private_max_workers=None, max_concurrent_tasks=None, on_error=None): 5 | self._listeners = {} 6 | self._once = {} 7 | self._stage = Stage( 8 | private_max_workers=private_max_workers, 9 | max_concurrent_tasks=max_concurrent_tasks, 10 | on_error=on_error, 11 | is_daemon=True, 12 | ) 13 | 14 | def add_listener(self, event, listener): 15 | if event not in self._listeners: 16 | self._listeners.update({ event: [] }) 17 | if listener not in self._listeners[event]: 18 | self._listeners[event].append(listener) 19 | return listener 20 | 21 | def remove_listener(self, event, listener): 22 | if event in self._listeners and listener in self._listeners[event]: 23 | self._listeners[event].remove(listener) 24 | 25 | def remove_all_listeners(self, event_list): 26 | if isinstance(event_list, str): 27 | event_list = [event_list] 28 | for event in event_list: 29 | self._listeners.update({ event: [] }) 30 | 31 | def on(self, event, listener): 32 | return self.add_listener(event, listener) 33 | 34 | def off(self, event, listener): 35 | return self.remove_listener(event, listener) 36 | 37 | def once(self, event, listener): 38 | if event not in self._once: 39 | self._once.update({ event: [] }) 40 | if listener not in self._listeners[event] and listener not in self._once[event]: 41 | self._once[event].append(listener) 42 | return listener 43 | 44 | def listener_count(self, event): 45 | return len(self._listeners[event]) + len(self._once[event]) 46 | 47 | def emit(self, event, *args, **kwargs): 48 | listeners_to_execute = [] 49 | if event in self._listeners: 50 | for listener in self._listeners[event]: 51 | listeners_to_execute.append((listener, args, kwargs)) 52 | if event in self._once: 53 | for listener in self._once[event]: 54 | listeners_to_execute.append((listener, args, kwargs)) 55 | self._once.update({ event: [] }) 56 | for listener, args, kwargs in listeners_to_execute: 57 | self._stage.go(listener, *args, **kwargs) 58 | return len(listeners_to_execute) -------------------------------------------------------------------------------- /Agently/Stage/MessageCenter.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from .Stage import Stage 3 | 4 | class MessageCenter: 5 | def __init__(self, private_max_workers=None, max_concurrent_tasks=None, on_error=None): 6 | self._stage = Stage( 7 | private_max_workers=private_max_workers, 8 | max_concurrent_tasks=max_concurrent_tasks, 9 | on_error=on_error, 10 | is_daemon=True 11 | ) 12 | self._consumers = {} 13 | 14 | def register_consumer(self, handler): 15 | consumer_id = uuid.uuid4() 16 | self._consumers.update({ consumer_id: handler }) 17 | return consumer_id 18 | 19 | def remove_consumer(self, consumer_id): 20 | del self._consumers[consumer_id] 21 | 22 | def put(self, data): 23 | for _, consumer_handler in self._consumers.items(): 24 | self._stage.go(consumer_handler, data) 25 | 26 | def close(self): 27 | self._stage.close() -------------------------------------------------------------------------------- /Agently/Stage/StageFunction.py: -------------------------------------------------------------------------------- 1 | class StageFunction: 2 | def __init__(self, stage, func, *args, **kwargs): 3 | self._stage = stage 4 | self._func = func 5 | self._args = args 6 | self._kwargs = kwargs 7 | self._on_success_handler = None 8 | self._on_error_handler = None 9 | 10 | def set(self, *args, **kwargs): 11 | self._args = args 12 | self._kwargs = kwargs 13 | return self 14 | 15 | def args(self, *args): 16 | self._args = args 17 | return self 18 | 19 | def kwargs(self, **kwargs): 20 | self._kwargs = kwargs 21 | return self 22 | 23 | def on_success(self, on_success_handler): 24 | self._on_success_handler = on_success_handler 25 | return self 26 | 27 | def on_error(self, on_error_handler): 28 | self._on_error_handler = on_error_handler 29 | return self 30 | 31 | def go(self): 32 | return self._stage.go( 33 | self._func, 34 | *self._args, 35 | on_success=self._on_success_handler, 36 | on_error=self._on_error_handler, 37 | **self._kwargs 38 | ) 39 | 40 | def get(self): 41 | return self._stage.get( 42 | self._func, 43 | *self._args, 44 | on_success=self._on_success_handler, 45 | on_error=self._on_error_handler, 46 | **self._kwargs 47 | ) 48 | 49 | class StageFunctionMixin: 50 | def func(self, func): 51 | return StageFunction(self, func) -------------------------------------------------------------------------------- /Agently/Stage/StageHybridGenerator.py: -------------------------------------------------------------------------------- 1 | import queue 2 | import asyncio 3 | import threading 4 | 5 | class StageHybridGenerator: 6 | def __init__(self, stage, task, on_success=None, on_error=None, lazy=None, async_gen_interval=0.1): 7 | self._stage = stage 8 | self._stage._responses.add(self) 9 | self._loop = stage._loop 10 | self._on_success = on_success 11 | self._on_error = on_error 12 | self._task = task 13 | self._result = [] 14 | self._error = None 15 | self._result_queue = queue.Queue() 16 | self._result_ready = threading.Event() 17 | self._completed = False 18 | self._final_result = None 19 | self._is_lazy = lazy 20 | self._async_gen_interval = async_gen_interval 21 | if not self._is_lazy: 22 | self._run_consume_async_gen(self._task) 23 | 24 | def _run_consume_async_gen(self, task): 25 | consume_result = asyncio.run_coroutine_threadsafe(self._consume_async_gen(task), self._loop) 26 | consume_result.add_done_callback(self._on_consume_async_gen_done) 27 | 28 | def _on_consume_async_gen_done(self, future): 29 | future.result() 30 | if self._error is not None: 31 | def raise_error(): 32 | raise self._error 33 | self._loop.call_soon_threadsafe(raise_error) 34 | if self._on_success: 35 | self._final_result = self._on_success(self._result) 36 | self._result_ready.set() 37 | self._stage._responses.discard(self) 38 | 39 | async def _consume_async_gen(self, task): 40 | try: 41 | async for item in task: 42 | self._result_queue.put(item) 43 | self._result.append(item) 44 | self._completed = True 45 | except Exception as e: 46 | if self._on_error: 47 | handled_result = self._on_error(e) 48 | self._result_queue.put(handled_result) 49 | self._result.append(handled_result) 50 | else: 51 | self._result_queue.put(e) 52 | self._result.append(e) 53 | self._error = e 54 | finally: 55 | self._result_queue.put(StopIteration) 56 | 57 | async def __aiter__(self): 58 | if self._is_lazy: 59 | self._run_consume_async_gen(self._task) 60 | while True: 61 | try: 62 | item = self._result_queue.get_nowait() 63 | if item is StopIteration: 64 | break 65 | yield item 66 | except queue.Empty: 67 | await asyncio.sleep(self._async_gen_interval) 68 | 69 | def __iter__(self): 70 | if self._is_lazy: 71 | self._run_consume_async_gen(self._task) 72 | while True: 73 | item = self._result_queue.get() 74 | if item is StopIteration: 75 | break 76 | yield item 77 | 78 | def get(self): 79 | self._result_ready.wait() 80 | return self._result 81 | 82 | def get_final(self): 83 | self._result_ready.wait() 84 | if self._final_result: 85 | return self._final_result 86 | else: 87 | return self._result -------------------------------------------------------------------------------- /Agently/Stage/StageResponse.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import threading 3 | 4 | class StageResponse: 5 | def __init__(self, stage, task, on_success=None, on_error=None): 6 | self._stage = stage 7 | self._stage._responses.add(self) 8 | self._loop = self._stage._loop 9 | self._on_success = on_success 10 | self._on_error = on_error 11 | if asyncio.iscoroutine(task): 12 | self._task = asyncio.run_coroutine_threadsafe(task, self._loop) 13 | elif asyncio.isfuture(task): 14 | self._task = task 15 | self._task.add_done_callback(self._on_task_done) 16 | self._result_ready = threading.Event() 17 | self._status = None 18 | self._result = None 19 | self._error = None 20 | self._final_result = None 21 | 22 | def _on_task_done(self, future): 23 | try: 24 | self._status = True 25 | self._result = future.result() 26 | if self._on_success: 27 | self._final_result = self._on_success(self._result) 28 | self._result_ready.set() 29 | self._stage._responses.discard(self) 30 | except Exception as e: 31 | self._status = False 32 | self._error = e 33 | if self._on_error: 34 | self._final_result = self._on_error(self._error) 35 | self._result_ready.set() 36 | self._stage._responses.discard(self) 37 | else: 38 | self._result_ready.set() 39 | self._stage._responses.discard(self) 40 | raise self._error 41 | 42 | def get(self): 43 | self._result_ready.wait() 44 | if self._status == True: 45 | return self._result 46 | elif self._on_error is None: 47 | raise self._error 48 | 49 | def get_final(self): 50 | self._result_ready.wait() 51 | if self._final_result: 52 | return self._final_result 53 | else: 54 | return self._result -------------------------------------------------------------------------------- /Agently/Stage/Tunnel.py: -------------------------------------------------------------------------------- 1 | import queue 2 | import threading 3 | from .Stage import Stage 4 | 5 | class Tunnel: 6 | def __init__(self, private_max_workers=1, max_concurrent_tasks=None, on_error=None, timeout=10): 7 | self._private_max_worker = private_max_workers 8 | self._max_concurrent_tasks = max_concurrent_tasks 9 | self._on_error = on_error 10 | self._timeout = timeout 11 | self._data_queue = queue.Queue() 12 | self._close_event = threading.Event() 13 | self._stage = None 14 | 15 | def _defer_close_stage(self): 16 | def close_stage(): 17 | self._close_event.wait() 18 | if self._stage is not None: 19 | self._stage.close() 20 | self._stage = None 21 | defer_thread = threading.Thread(target=close_stage) 22 | defer_thread.start() 23 | 24 | def _get_stage(self): 25 | if self._stage is not None: 26 | return self._stage 27 | else: 28 | self._stage = Stage(private_max_workers=self._private_max_worker, max_concurrent_tasks=self._max_concurrent_tasks, on_error=self._on_error) 29 | return self._stage 30 | 31 | def put(self, data): 32 | self._data_queue.put(data) 33 | 34 | def put_stop(self): 35 | self._data_queue.put(StopIteration) 36 | 37 | def get(self): 38 | self._defer_close_stage() 39 | stage = self._get_stage() 40 | def queue_consumer(): 41 | while True: 42 | try: 43 | if self._timeout is not None: 44 | data = self._data_queue.get(timeout=self._timeout) 45 | else: 46 | data = self._data_queue.get() 47 | except queue.Empty: 48 | break 49 | if data is StopIteration: 50 | break 51 | yield data 52 | self._close_event.set() 53 | return stage.go(queue_consumer) -------------------------------------------------------------------------------- /Agently/Stage/__init__.py: -------------------------------------------------------------------------------- 1 | from .Stage import Stage 2 | from .Tunnel import Tunnel 3 | from .MessageCenter import MessageCenter 4 | from .EventEmitter import EventEmitter -------------------------------------------------------------------------------- /Agently/Workflow/Chunk/Loop.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from typing import TYPE_CHECKING 3 | from ..executors.generater.loop import use_loop_executor 4 | from ..lib.constants import EXECUTOR_TYPE_LOOP 5 | from .helper import exposed_interface 6 | 7 | if TYPE_CHECKING: 8 | from . import SchemaChunk 9 | 10 | class LoopAbility: 11 | """提供Loop API""" 12 | 13 | def __init__(self) -> None: 14 | self.shared_ns['loop'] = {} 15 | super().__init__() 16 | 17 | @exposed_interface(type='connect_command') 18 | def loop_with(self, sub_workflow) -> 'SchemaChunk': 19 | """遍历逐项处理,支持传入子 workflow/处理方法 作为处理逻辑""" 20 | is_function = inspect.isfunction(sub_workflow) or inspect.iscoroutinefunction(sub_workflow) 21 | # 这里是新的 chunk,需要走 Schema 的 create 方法挂载 22 | loop_chunk = self.create_rel_chunk( 23 | type=EXECUTOR_TYPE_LOOP, 24 | title="Loop", 25 | executor=use_loop_executor(sub_workflow), 26 | extra_info={ 27 | # loop 的相关信息 28 | "loop_info": { 29 | "type": 'function' if is_function else 'workflow', 30 | "detail": sub_workflow.__name__ if is_function else sub_workflow.schema.compile() 31 | } 32 | } 33 | ) 34 | return self._raw_connect_to(loop_chunk) 35 | -------------------------------------------------------------------------------- /Agently/Workflow/Chunk/helper.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | 4 | def exposed_interface(type='command'): 5 | """修饰对外提供的接口调用,支持传参""" 6 | def decorator(method): 7 | @functools.wraps(method) 8 | def wrapper(self, *args, **kwargs): 9 | # 调用所有前置拦截方法 10 | for interceptor in self._pre_command_interceptors: 11 | interceptor(self, type, method.__name__, *args, **kwargs) 12 | 13 | # 调用实际的方法 14 | result = method(self, *args, **kwargs) 15 | 16 | # 调用所有后置拦截方法 17 | for interceptor in self._post_command_interceptors: 18 | interceptor(self, type, method.__name__, result) 19 | 20 | return result 21 | return wrapper 22 | return decorator -------------------------------------------------------------------------------- /Agently/Workflow/Runtime/BranchState.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from typing import List 3 | from ..utils.chunk_helper import deep_copy_simply 4 | 5 | class RuntimeBranchState: 6 | """某个分支的运行时状态""" 7 | def __init__(self, **args) -> None: 8 | self.id = args.get('id') 9 | # 恢复各 chunk 的运行状态 10 | self.chunk_status = args.get('chunk_status', {}) 11 | self.executing_ids: List[str] = args.get('executing_ids', []) 12 | self.visited_record: List[str] = args.get('visited_record', []) 13 | # 总运行状态 14 | self.running_status = args.get('running_status', 'idle') 15 | # 快队列 16 | self.running_queue = deque(args.get('running_queue', [])) 17 | # 慢队列 18 | self.slow_queue = deque(args.get('slow_queue', [])) 19 | 20 | def update_chunk_status(self, chunk_id, status): 21 | if status not in ['idle', 'success', 'running', 'error', 'pause']: 22 | return 23 | self.chunk_status[chunk_id] = status 24 | 25 | def get_chunk_status(self, chunk_id): 26 | return self.chunk_status.get(chunk_id) 27 | 28 | def export(self): 29 | return deep_copy_simply({ 30 | 'id': self.id, 31 | 'executing_ids': self.executing_ids, 32 | 'chunk_status': self.chunk_status, 33 | 'visited_record': self.visited_record, 34 | 'running_status': self.running_status, 35 | 'running_queue': list(self.running_queue), 36 | 'slow_queue': list(self.slow_queue) 37 | }) 38 | -------------------------------------------------------------------------------- /Agently/Workflow/Runtime/Checkpoint.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | from .Repository import CheckpointRepository 3 | from .Snapshot import Snapshot 4 | from ..lib.constants import DEFAULT_CHECKPOINT_NAME 5 | from ..utils.runner import run_async 6 | 7 | if TYPE_CHECKING: 8 | from .State import RuntimeState 9 | 10 | class Checkpoint: 11 | """ Checkpoint 管理逻辑,包含快照数据存储、回溯、恢复等 """ 12 | 13 | def __init__(self, checkpoint_id, repository=CheckpointRepository(), default_snapshot_name: str=None) -> None: 14 | # 默认存储的快照名 15 | self.default_snapshot_name = default_snapshot_name or DEFAULT_CHECKPOINT_NAME 16 | self.checkpoint_id = checkpoint_id 17 | self.repository: CheckpointRepository = repository 18 | self.active_snapshot: Snapshot = None 19 | self._disabled = False 20 | 21 | def use(self, name: str): 22 | """使用指定的快照名(存储、回滚、删除默认都会使用该快照名)""" 23 | self.default_snapshot_name = name 24 | return self 25 | 26 | def disable(self): 27 | self._disabled = True 28 | return self 29 | 30 | def save(self, state: 'RuntimeState', name=None, time=None, **args): 31 | """将某个状态存储到快照记录中""" 32 | if self._disabled: 33 | return self 34 | return run_async(self.save_async(state=state, name=name, time=time, **args)) 35 | 36 | async def save_async(self, state: 'RuntimeState', name=None, time=None): 37 | """将某个状态存储到快照记录中""" 38 | if self._disabled: 39 | return self 40 | snapshot_name = name or self.default_snapshot_name 41 | snapshot = Snapshot(name=snapshot_name, state=state, time=time) 42 | await self.repository.save(checkpoint_id=self.checkpoint_id, name=snapshot_name, data=snapshot.export()) 43 | return self 44 | 45 | def remove(self, name=None): 46 | if self._disabled: 47 | return self 48 | snapshot_name = name or self.default_snapshot_name 49 | return run_async(self.remove_async(name=snapshot_name)) 50 | 51 | async def remove_async(self, name=None): 52 | """删除指定的快照存储""" 53 | if self._disabled: 54 | return self 55 | snapshot_name = name or self.default_snapshot_name 56 | await self.repository.remove(checkpoint_id=self.checkpoint_id, name=snapshot_name) 57 | return self 58 | 59 | def rollback(self, name=None, silence=False, **args): 60 | """回滚到指定checkpoint""" 61 | if self._disabled: 62 | return self 63 | snapshot_name = name or self.default_snapshot_name 64 | run_async(self.rollback_async(name=snapshot_name, silence=silence, **args)) 65 | 66 | async def rollback_async(self, name=None, silence=False): 67 | """回滚到指定checkpoint""" 68 | if self._disabled: 69 | return self 70 | snapshot_name = name or self.default_snapshot_name 71 | snapshot_raw_data = await self.repository.get(checkpoint_id=self.checkpoint_id, name=snapshot_name) 72 | if not snapshot_raw_data: 73 | # 静默态,无需抛错 74 | if silence: 75 | return self 76 | raise ValueError( 77 | f'Attempt to roll back to a non-existent checkpoint record named "{name}".') 78 | 79 | self.active_snapshot = Snapshot(**snapshot_raw_data) 80 | return self 81 | 82 | def get_active_snapshot(self): 83 | """返回当前激活的快照""" 84 | return self.active_snapshot 85 | -------------------------------------------------------------------------------- /Agently/Workflow/Runtime/Repository.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from Agently.utils import StorageDelegate 3 | from Agently._global import global_plugin_manager, global_settings 4 | 5 | class CheckpointRepository: 6 | def __init__(self, storage = None) -> None: 7 | self.storage = storage or StorageDelegate( 8 | db_name="checkpoint", 9 | plugin_manager=global_plugin_manager, 10 | settings=global_settings 11 | ) 12 | 13 | async def save(self, checkpoint_id, name: str, data: any): 14 | self.storage.set(table_name=checkpoint_id, key=name, value=data) 15 | return self 16 | 17 | async def remove(self, checkpoint_id, name: str): 18 | self.storage.remove(table_name=checkpoint_id, key=name) 19 | return self 20 | 21 | async def get(self, checkpoint_id, name: str): 22 | return self.storage.get(checkpoint_id, key=name) 23 | 24 | async def get_all(self, checkpoint_id, names: List[str] = None): 25 | return self.storage.get_all(checkpoint_id, keys=names) 26 | -------------------------------------------------------------------------------- /Agently/Workflow/Runtime/Snapshot.py: -------------------------------------------------------------------------------- 1 | import time as time_raw 2 | from typing import TYPE_CHECKING, Union 3 | from ..utils.chunk_helper import deep_copy_simply 4 | 5 | if TYPE_CHECKING: 6 | from .State import RuntimeState 7 | 8 | class Snapshot: 9 | """主要负责单个 RuntimeState 的静态化挂载及管理逻辑""" 10 | 11 | def __init__(self, name: str, state: Union['RuntimeState', dict], time: int = None) -> None: 12 | self.name = name 13 | self.state_schema = state if isinstance(state, dict) else state.export() 14 | self.time = time or time_raw.time() 15 | 16 | def get_state_val(self, keys_with_dots: str): 17 | """获取指定状态值""" 18 | keys = keys_with_dots.split('.') 19 | pointer = self.state_schema 20 | for key in keys: 21 | if not pointer or key not in pointer: 22 | return None 23 | pointer = pointer.get(key) 24 | return deep_copy_simply(pointer) 25 | 26 | def export(self) -> dict: 27 | return deep_copy_simply({ 28 | 'name': self.name, 29 | 'state': self.state_schema, 30 | 'time': self.time 31 | }) -------------------------------------------------------------------------------- /Agently/Workflow/Runtime/State.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, TYPE_CHECKING 2 | from .BranchState import RuntimeBranchState 3 | 4 | if TYPE_CHECKING: 5 | from .Snapshot import Snapshot 6 | 7 | CURRENT_STATE_VERSION = 1 8 | 9 | class RuntimeState: 10 | """整个 workflow 的运行时状态""" 11 | 12 | def __init__(self, **args) -> None: 13 | if args.get('v') and args.get('v') != CURRENT_STATE_VERSION: 14 | raise ValueError(f'The state data of version {args.get("v")} cannot be used on the checkpoint management logic of version {CURRENT_STATE_VERSION}.') 15 | # schema 版本 16 | self.v = CURRENT_STATE_VERSION 17 | self.workflow_id = args.get('workflow_id') 18 | # 分支决策逻辑 19 | self.branches_state: Dict[str, RuntimeBranchState] = args.get( 20 | 'branches_state', {}) 21 | # 各 chunk 依赖数据 22 | self.chunks_dep_state = args.get('chunks_dep_state', {}) 23 | # 各 chunk 附加数据(如 Loop chunk 的 State 数据) 24 | self.chunks_extra_data = args.get('chunks_extra_data', {}) 25 | # 总运行状态 26 | self.running_status = args.get('running_status', 'idle') 27 | # 运行数据 28 | self.user_store = args.get('user_store') 29 | self.sys_store = args.get('sys_store') 30 | self.restore_mode = False 31 | 32 | def restore_from_snapshot(self, snapshot: 'Snapshot') -> 'RuntimeState': 33 | """从指定表述结构中恢复""" 34 | schema = snapshot.export().get('state') 35 | # 版本校验 36 | if schema.get('v') and schema.get('v') != CURRENT_STATE_VERSION: 37 | raise ValueError( 38 | f'The state data of version {schema.get("v")} cannot be used on the checkpoint management logic of version {CURRENT_STATE_VERSION}.') 39 | self.workflow_id = schema.get('workflow_id') 40 | 41 | # 恢复挂载实例化的 branch_state 42 | branches_state = schema.get('branches_state') 43 | self.branches_state = {} 44 | for entry_id in branches_state: 45 | self.branches_state[entry_id] = RuntimeBranchState(**branches_state[entry_id]) 46 | 47 | # 恢复依赖数据 48 | self.chunks_dep_state = schema.get('chunks_dep_state') 49 | # 恢复运行状态 50 | self.running_status = schema.get('running_status') 51 | # 恢复用户 store 52 | self.user_store = self.user_store.__class__(schema.get('user_store')) 53 | # 恢复系统 store 54 | self.sys_store = self.sys_store.__class__(schema.get('sys_store')) 55 | # 恢复模式 56 | self.restore_mode = True 57 | return self 58 | 59 | def update(self, chunks_dep_state: dict = {}): 60 | # 各 chunk 依赖数据 61 | self.chunks_dep_state = chunks_dep_state or {} 62 | 63 | def create_branch_state(self, chunk) -> RuntimeBranchState: 64 | self.branches_state[chunk['id']] = RuntimeBranchState(id=chunk['id']) 65 | return self.branches_state[chunk['id']] 66 | 67 | def get_branch_state(self, chunk) -> RuntimeBranchState: 68 | return self.branches_state.get(chunk['id']) 69 | 70 | def export(self): 71 | # 将分支状态实例导出状态原值 72 | branches_state_value = {} 73 | for id in self.branches_state: 74 | branches_state_value[id] = self.branches_state[id].export() 75 | # 返回快照数据 76 | return { 77 | 'v': self.v, 78 | 'workflow_id': self.workflow_id, 79 | 'branches_state': branches_state_value, 80 | 'chunks_dep_state': self.chunks_dep_state, 81 | 'running_status': self.running_status, 82 | 'user_store': self.user_store.get_all(), 83 | 'sys_store': self.sys_store.get_all() 84 | } 85 | -------------------------------------------------------------------------------- /Agently/Workflow/Runtime/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Dict 2 | from .BranchState import RuntimeBranchState 3 | from .State import RuntimeState 4 | from .Checkpoint import Checkpoint -------------------------------------------------------------------------------- /Agently/Workflow/__init__.py: -------------------------------------------------------------------------------- 1 | from .Schema import Schema 2 | from .Workflow import Workflow 3 | from .Runtime import Checkpoint -------------------------------------------------------------------------------- /Agently/Workflow/executors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgentEra/Agently/528a8294f2531d3703efb5bcaa0fea4f9c5357a8/Agently/Workflow/executors/__init__.py -------------------------------------------------------------------------------- /Agently/Workflow/executors/builtin/ConditionExecutor.py: -------------------------------------------------------------------------------- 1 | from ...lib.Store import Store 2 | 3 | def condition_executor(inputs, store: Store, **sys_info): 4 | """条件执行器,会从其 extra_info['condition_info'] 中取到判断条件和结果符号,最终返回命中判断的结果符号,同时透传上游的值""" 5 | chunk_data = sys_info['chunk']['data'] 6 | condition_info = (chunk_data.get('extra_info') or {}).get('condition_info') or {} 7 | normal_conditions = condition_info.get('conditions') 8 | # 从众多条件中依次判断,如果命中了,则返回对应条件的结果 id 9 | if normal_conditions and len(normal_conditions) > 0: 10 | for condition_desc in normal_conditions: 11 | condition = condition_desc.get('condition') 12 | if condition and condition(inputs['default'], store): 13 | return { 14 | "condition_signal": condition_desc.get('condition_signal'), 15 | "values": inputs['default'] 16 | } 17 | # 否则返回 else 的结果 id 18 | else_condition = condition_info.get('else_condition') 19 | return { 20 | "condition_signal": else_condition.get('condition_signal'), 21 | "values": inputs['default'] 22 | } 23 | -------------------------------------------------------------------------------- /Agently/Workflow/executors/builtin/EndExecutor.py: -------------------------------------------------------------------------------- 1 | from ...lib.Store import Store 2 | from ...lib.constants import WORKFLOW_END_DATA_HANDLE_NAME 3 | 4 | def end_executor(inputs, store: Store, **sys_info): 5 | sys_info['sys_store'].set(WORKFLOW_END_DATA_HANDLE_NAME, inputs) 6 | return inputs -------------------------------------------------------------------------------- /Agently/Workflow/executors/builtin/StartExecutor.py: -------------------------------------------------------------------------------- 1 | from ...lib.Store import Store 2 | from ...lib.constants import WORKFLOW_START_DATA_HANDLE_NAME 3 | 4 | def start_executor(inputs, store: Store, **sys_info): 5 | return sys_info['sys_store'].get(WORKFLOW_START_DATA_HANDLE_NAME) 6 | -------------------------------------------------------------------------------- /Agently/Workflow/executors/builtin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgentEra/Agently/528a8294f2531d3703efb5bcaa0fea4f9c5357a8/Agently/Workflow/executors/builtin/__init__.py -------------------------------------------------------------------------------- /Agently/Workflow/executors/builtin/install.py: -------------------------------------------------------------------------------- 1 | from .StartExecutor import start_executor 2 | from .EndExecutor import end_executor 3 | from .ConditionExecutor import condition_executor 4 | from ...MainExecutor import MainExecutor 5 | from ...lib.constants import EXECUTOR_TYPE_START, EXECUTOR_TYPE_END, EXECUTOR_TYPE_CONDITION 6 | 7 | def mount_built_in_executors(main_executor: MainExecutor): 8 | """ 9 | 挂载内置的执行器 10 | """ 11 | main_executor.regist_executor(EXECUTOR_TYPE_START, start_executor) 12 | main_executor.regist_executor(EXECUTOR_TYPE_END, end_executor) 13 | main_executor.regist_executor(EXECUTOR_TYPE_CONDITION, condition_executor) 14 | -------------------------------------------------------------------------------- /Agently/Workflow/executors/generater/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgentEra/Agently/528a8294f2531d3703efb5bcaa0fea4f9c5357a8/Agently/Workflow/executors/generater/__init__.py -------------------------------------------------------------------------------- /Agently/Workflow/executors/generater/loop.py: -------------------------------------------------------------------------------- 1 | 2 | import inspect 3 | from ...lib.Store import Store 4 | from ...lib.constants import DEFAULT_INPUT_HANDLE_VALUE 5 | 6 | def use_loop_executor(sub_workflow): 7 | async def loop_executor(inputs, store: Store, **sys_info): 8 | # Run Loop 9 | input_val = inputs.get(DEFAULT_INPUT_HANDLE_VALUE) 10 | all_result = [] 11 | if isinstance(input_val, list): 12 | for val in input_val: 13 | all_result.append(await loop_unit_core(unit_val=val, store=store)) 14 | elif isinstance(input_val, dict): 15 | for key, value in input_val.items(): 16 | all_result.append(await loop_unit_core(unit_val={ 17 | "key": key, 18 | "value": value 19 | }, store=store)) 20 | elif isinstance(input_val, int): 21 | for i in range(input_val): 22 | all_result.append(await loop_unit_core(unit_val=i, store=store)) 23 | # Old Version Compatible 24 | if inspect.isfunction(sub_workflow) or inspect.iscoroutinefunction(sub_workflow): 25 | return all_result 26 | elif hasattr(sub_workflow, "settings") and sub_workflow.settings.get("compatible_version") and sub_workflow.settings.get("compatible_version") <= 3316: 27 | return all_result 28 | else: 29 | # Regroup 30 | final_result = {} 31 | i = 0 32 | for one_result in all_result: 33 | if one_result: 34 | used_handle_pool = [] 35 | for handle, result in one_result.items(): 36 | if handle not in final_result: 37 | final_result.update({ handle: [None] * i }) 38 | final_result[handle].append(result) 39 | used_handle_pool.append(handle) 40 | for final_result_handle in final_result.keys(): 41 | if final_result_handle not in used_handle_pool: 42 | final_result[final_result_handle].append(None) 43 | else: 44 | if "default" not in final_result: 45 | final_result.update({ "default": [None] * i }) 46 | final_result["default"].append(None) 47 | for final_result_handle in final_result.keys(): 48 | if final_result_handle != "default": 49 | final_result[final_result_handle].append(None) 50 | i += 1 51 | return final_result 52 | 53 | async def loop_unit_core(unit_val, store={}): 54 | if inspect.iscoroutinefunction(sub_workflow): 55 | return await sub_workflow(unit_val, store) 56 | elif inspect.isfunction(sub_workflow): 57 | return sub_workflow(unit_val, store) 58 | else: 59 | return await sub_workflow.start_async(unit_val, storage=store.get_all() if store else {}) 60 | 61 | return loop_executor 62 | -------------------------------------------------------------------------------- /Agently/Workflow/lib/BreakingHub.py: -------------------------------------------------------------------------------- 1 | # 用于处理中断的逻辑 2 | import time 3 | 4 | BREAKING_TYPES = { 5 | "TIMEOUT": "timeout", 6 | "CALL_MAXIMUM": "call_maximum" 7 | } 8 | 9 | 10 | class BreakingHub: 11 | def __init__(self, breaking_handler, max_execution_limit=5, interval_time = 2, timeout_threshold=10): 12 | self.max_execution_limit = max_execution_limit 13 | self.timeout_threshold = timeout_threshold 14 | self.breaking_handler = breaking_handler 15 | # 计数间隔时间 16 | self.interval_time = interval_time 17 | self.execution_counts = {} 18 | self.last_break_time = {} 19 | 20 | # 节点执行次数记录器 21 | def recoder(self, chunk): 22 | chunk_id = chunk['id'] 23 | if chunk_id not in self.execution_counts: 24 | self.execution_counts[chunk_id] = 1 25 | self.last_break_time[chunk_id] = time.time() 26 | else: 27 | current_time = time.time() 28 | if current_time - self.last_break_time[chunk_id] <= self.interval_time: 29 | self.execution_counts[chunk_id] += 1 30 | else: 31 | self.execution_counts[chunk_id] = 1 32 | self.last_break_time[chunk_id] = current_time 33 | 34 | if self.execution_counts[chunk_id] > self.max_execution_limit: 35 | self.handle_interrupt(chunk, BREAKING_TYPES["CALL_MAXIMUM"]) 36 | 37 | def get_counts(self, chunk): 38 | chunk_id = chunk['id'] 39 | return self.execution_counts.get(chunk_id, 0) 40 | 41 | def handle_interrupt(self, chunk, type): 42 | self.breaking_handler(chunk, type) 43 | -------------------------------------------------------------------------------- /Agently/Workflow/lib/ChunkExecutorManager.py: -------------------------------------------------------------------------------- 1 | class ChunkExecutorManager(object): 2 | def __init__(self): 3 | self.chunk_executors = {} 4 | 5 | def register(self, executor_id: str, executor_func: callable): 6 | self.chunk_executors.update({ executor_id: executor_func }) 7 | return self 8 | 9 | def get(self, executor_id: str): 10 | if executor_id in self.chunk_executors: 11 | return self.chunk_executors[executor_id] 12 | else: 13 | return None -------------------------------------------------------------------------------- /Agently/Workflow/lib/Store.py: -------------------------------------------------------------------------------- 1 | """ 2 | import copy 3 | 4 | class Store: 5 | def __init__(self, init_schema = {}) -> None: 6 | self.store = init_schema.copy() 7 | 8 | def set(self, name: str, data: any): 9 | self.store[name] = data 10 | 11 | def set_with_dict(self, data: dict): 12 | self.store.update(data) 13 | 14 | def remove(self, name: str): 15 | if name in self.store: 16 | del self.store[name] 17 | return self 18 | 19 | def remove_all(self): 20 | self.store = {} 21 | return self 22 | 23 | def get(self, name: str, default = None): 24 | return self.store.get(name) or default 25 | 26 | def get_all(self): 27 | return copy.deepcopy(self.store) 28 | """ 29 | 30 | import Agently.utils.DataOps as DataOps 31 | 32 | class Store(DataOps): 33 | def __init__(self, init_schema = None): 34 | super().__init__(target_data = init_schema) 35 | self.store = self.target_data 36 | self.remove_all = self.empty 37 | self.set_with_dict = self.update_by_dict 38 | self.get_all = lambda: self.get() -------------------------------------------------------------------------------- /Agently/Workflow/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgentEra/Agently/528a8294f2531d3703efb5bcaa0fea4f9c5357a8/Agently/Workflow/lib/__init__.py -------------------------------------------------------------------------------- /Agently/Workflow/lib/constants.py: -------------------------------------------------------------------------------- 1 | # 入口类型 2 | EXECUTOR_TYPE_START = 'Start' 3 | # 结束类型 4 | EXECUTOR_TYPE_END = 'End' 5 | 6 | # 条件类型 7 | EXECUTOR_TYPE_CONDITION = 'Condition' 8 | 9 | # 内置声明好的执行器 10 | BUILT_IN_EXECUTOR_TYPES = [ 11 | EXECUTOR_TYPE_START, 12 | EXECUTOR_TYPE_END, 13 | EXECUTOR_TYPE_CONDITION 14 | ] 15 | # 循环类型 16 | EXECUTOR_TYPE_LOOP = 'Loop' 17 | 18 | # 常规类型 19 | EXECUTOR_TYPE_NORMAL = 'Normal' 20 | 21 | SYS_SYMBOL = 'WORKFLOW_SYS_DATA' 22 | 23 | # workflow 初始数据的句柄名 24 | WORKFLOW_START_DATA_HANDLE_NAME = f'{SYS_SYMBOL}:{EXECUTOR_TYPE_START}' 25 | WORKFLOW_END_DATA_HANDLE_NAME = f'{SYS_SYMBOL}:{EXECUTOR_TYPE_END}' 26 | 27 | # 默认的输入 handle 28 | DEFAULT_INPUT_HANDLE_VALUE = 'default' 29 | DEFAULT_INPUT_HANDLE = {'handle': DEFAULT_INPUT_HANDLE_VALUE} 30 | # 默认的输出 handle,表示数据输出的总出口(会输出所有数据) 31 | DEFAULT_OUTPUT_HANDLE_VALUE = '*' 32 | DEFAULT_OUTPUT_HANDLE = {'handle': DEFAULT_OUTPUT_HANDLE_VALUE} 33 | 34 | # 默认存储的 checkpoint 名 35 | DEFAULT_CHECKPOINT_NAME = 'default' 36 | -------------------------------------------------------------------------------- /Agently/Workflow/types/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgentEra/Agently/528a8294f2531d3703efb5bcaa0fea4f9c5357a8/Agently/Workflow/types/__init__.py -------------------------------------------------------------------------------- /Agently/Workflow/types/chunk_data.py: -------------------------------------------------------------------------------- 1 | from typing import TypeAlias, Dict, List, TypedDict, Literal 2 | 3 | 4 | class ChunkDepData(TypedDict): 5 | handle: str 6 | data_slots: List[Dict] 7 | 8 | class ChunkData(TypedDict): 9 | id: str 10 | loop_entry: bool 11 | type: Literal['Normal', 'Loop', 'Start', 'End'] 12 | executor: callable 13 | deps: List[ChunkDepData] -------------------------------------------------------------------------------- /Agently/Workflow/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgentEra/Agently/528a8294f2531d3703efb5bcaa0fea4f9c5357a8/Agently/Workflow/utils/__init__.py -------------------------------------------------------------------------------- /Agently/Workflow/utils/chunk_helper.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | def create_connection_symbol(chunk, handle_name): 4 | """生成 chunk 连接字符""" 5 | return f"{get_display_name(chunk)}({handle_name})" 6 | 7 | def create_symbol(chunk): 8 | """生成 chunk 符号""" 9 | return f"{chunk['id']}({get_display_name(chunk)})" 10 | 11 | def get_display_name(chunk): 12 | return fix_display_text(chunk["title"] or f'chunk-{chunk["id"]}' or 'Unknow chunk') 13 | 14 | def fix_display_text(label: str): 15 | """修复文本展示,避免特殊字符""" 16 | if not label: 17 | return label 18 | escaped_string = label.replace('"', '"') 19 | return f'"{escaped_string}"' 20 | 21 | 22 | def deep_copy_simply(obj): 23 | """简单深拷贝,只处理 dict / list""" 24 | if isinstance(obj, list): 25 | return [deep_copy_simply(item) for item in obj] 26 | elif isinstance(obj, dict): 27 | return {key: deep_copy_simply(value) for key, value in obj.items()} 28 | else: 29 | return obj 30 | -------------------------------------------------------------------------------- /Agently/Workflow/utils/exec_tree.py: -------------------------------------------------------------------------------- 1 | from ..lib.constants import EXECUTOR_TYPE_NORMAL, EXECUTOR_TYPE_START, DEFAULT_INPUT_HANDLE_VALUE 2 | import uuid 3 | 4 | def generate_executed_schema(schema: dict): 5 | """根据定义描述,生成执行描述""" 6 | edges_map_info = generate_edge_prepare_data(schema.get('edges') or []) 7 | all_chunks = schema.get('chunks') or [] 8 | 9 | # 挂载各 chunk 的运行时 dep 逻辑数据 10 | chunks_dep_state = {} 11 | 12 | def create_exec_chunk(chunk): 13 | next_chunks = [] 14 | next_chunk_map = {} 15 | for target in edges_map_info['edges_target_map'].get(chunk['id'], []): 16 | target_id = target['target'] 17 | next_chunk = next_chunk_map.get(target_id) 18 | # 如之前没存入,则新建 19 | if not next_chunk: 20 | next_chunk = { 21 | 'id': target_id, 22 | 'handles': [] 23 | } 24 | next_chunks.append(next_chunk) 25 | next_chunk_map[next_chunk['id']] = next_chunk 26 | # 往 chunk 中注入当前的 handle 27 | next_chunk['handles'].append({ 28 | 'handle': target['target_handle'], 29 | 'source_handle': target['source_handle'], 30 | 'condition': target.get('condition') or None 31 | }) 32 | 33 | hanldes_desc = chunk.get('handles') or {} 34 | inputs_desc = hanldes_desc.get('inputs') or [] 35 | if not len(inputs_desc): 36 | inputs_desc = [{'handle': DEFAULT_INPUT_HANDLE_VALUE}] 37 | # 挂载 dep 结构 38 | chunks_dep_state[chunk['id']] = [ 39 | { 40 | 'handle': input_desc.get('handle'), 41 | # 运行时的依赖值,会随运行时实时更新,初始尝试从定义中取默认值 42 | 'data_slots': [{ 43 | 'id': str(uuid.uuid4()), # 唯一 id,设置后就不变了,用于标识管理 44 | 'is_ready': True, 45 | 'execution_ticket': str(uuid.uuid4()), # 可执行票据,可派发 46 | 'updator': 'default', # 更新者 47 | 'value': input_desc.get('default') 48 | }] if (input_desc.get('default') != None) or (chunk.get('type') == EXECUTOR_TYPE_START) else [] 49 | } 50 | for input_desc in inputs_desc 51 | ] 52 | return { 53 | 'id': chunk['id'], 54 | 'title': chunk['title'], 55 | 'loop_entry': False, # 是否是循环的起点 56 | 'next_chunks': next_chunks, 57 | 'type': chunk.get('type') or EXECUTOR_TYPE_NORMAL, 58 | 'executor': chunk.get('executor'), 59 | 'data': chunk 60 | } 61 | 62 | runtime_chunks_map = {} 63 | for chunk in all_chunks: 64 | runtime_chunks_map[chunk['id']] = create_exec_chunk(chunk) 65 | 66 | # 标识循环起点 67 | def update_loop_walker(chunk, paths = []): 68 | for next_info in chunk.get('next_chunks'): 69 | next_chunk = runtime_chunks_map.get(next_info['id']) 70 | if next_chunk: 71 | if next_chunk['id'] in paths: 72 | next_chunk['loop_entry'] = True 73 | return 74 | update_loop_walker(next_chunk, paths + [next_chunk['id']]) 75 | 76 | entries = [] 77 | for chunk in all_chunks: 78 | if not edges_map_info['edges_source_map'].get(chunk['id'], []): 79 | entry_chunk = runtime_chunks_map.get(chunk['id']) 80 | if entry_chunk: 81 | entries.append(entry_chunk) 82 | update_loop_walker(entry_chunk, [entry_chunk['id']]) 83 | 84 | return { 85 | 'entries': entries, 86 | 'chunk_map': runtime_chunks_map, 87 | 'chunks_dep_state': chunks_dep_state 88 | } 89 | 90 | def create_empty_data_slot(chunk): 91 | return { 92 | 'is_ready': chunk.get('type') == EXECUTOR_TYPE_START, 93 | 'updator': '', 94 | 'value': None 95 | } 96 | 97 | def create_empty_data_slots(chunk): 98 | return [{ 99 | 'is_ready': True, 100 | 'updator': '', 101 | 'value': None 102 | }] if chunk.get('type') == EXECUTOR_TYPE_START else [] 103 | 104 | def reset_chunk_slot_val(slot): 105 | """清空掉某个dep slot 的值""" 106 | slot['value'] = None 107 | slot['is_ready'] = False 108 | slot['execution_ticket'] = '' 109 | slot['updator'] = '' 110 | 111 | def disable_chunk_dep_ticket(slot): 112 | """回收执行票据""" 113 | slot['execution_ticket'] = '' 114 | 115 | def create_new_chunk_slot_with_val(ref_chunk_id, new_val): 116 | """创建一个新的带有值的slot""" 117 | return { 118 | 'id': str(uuid.uuid4()), # 唯一 id,设置后就不变了,用于标识管理 119 | 'is_ready': True, 120 | 'execution_ticket': str(uuid.uuid4()), # 可执行票据,可派发 121 | 'updator': ref_chunk_id, # 更新者 122 | 'value': new_val 123 | } 124 | 125 | def generate_edge_prepare_data(edges: list): 126 | edges_target_map = {} 127 | edges_source_map = {} 128 | 129 | for edge in edges: 130 | edges_target_map.setdefault(edge['source'], []).append(edge) 131 | edges_source_map.setdefault(edge['target'], []).append(edge) 132 | 133 | return { 134 | 'edges_source_map': edges_source_map, 135 | 'edges_target_map': edges_target_map 136 | } 137 | -------------------------------------------------------------------------------- /Agently/Workflow/utils/find.py: -------------------------------------------------------------------------------- 1 | def find(data_list: list, find_func): 2 | """ 3 | 查找数据 4 | """ 5 | found_data = None 6 | 7 | for data in data_list: 8 | if find_func(data): 9 | found_data = data 10 | break 11 | 12 | return found_data 13 | 14 | 15 | def find_by_attr(data_list: list, attr_name: str, attr_val: any): 16 | """ 17 | 查找数据 18 | """ 19 | found_data = None 20 | 21 | for data in data_list: 22 | if data.get(attr_name) == attr_val: 23 | found_data = data 24 | break 25 | 26 | return found_data 27 | 28 | 29 | def find_all(data_list: list, find_func): 30 | """ 31 | 查找数据 32 | """ 33 | found_data = [] 34 | 35 | for data in data_list: 36 | if find_func(data): 37 | found_data.append(data) 38 | break 39 | 40 | return found_data 41 | 42 | 43 | def find_all_by_attr(data_list: list, attr_name: str, attr_val: any): 44 | """ 45 | 查找数据 46 | """ 47 | found_data = [] 48 | 49 | for data in data_list: 50 | if data.get(attr_name) == attr_val: 51 | found_data.append(data) 52 | break 53 | 54 | return found_data 55 | 56 | def has_target(data_list: list, judge_func): 57 | """ 58 | 判断数据是否存在 59 | """ 60 | for data in data_list: 61 | if judge_func(data): 62 | return True 63 | 64 | return False 65 | 66 | 67 | def has_target_by_attr(data_list: list, attr_name: str, attr_val: any): 68 | """ 69 | 判断数据是否存在 70 | """ 71 | for data in data_list: 72 | if data.get(attr_name) == attr_val: 73 | return True 74 | 75 | return False 76 | -------------------------------------------------------------------------------- /Agently/Workflow/utils/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | def get_default_logger(name: str, level=logging.DEBUG): 4 | logger = logging.getLogger(name) 5 | logger.setLevel(level) 6 | # 创建一个处理器(例如StreamHandler,用于控制台输出) 7 | console_handler = logging.StreamHandler() 8 | # 设置处理器的级别(只有达到或高于此级别的消息才会被发送到处理器) 9 | console_handler.setLevel(level) 10 | # 格式化处理 11 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 12 | console_handler.setFormatter(formatter) 13 | # 将处理器添加到Logger中 14 | logger.addHandler(console_handler) 15 | return logger 16 | -------------------------------------------------------------------------------- /Agently/Workflow/utils/runner.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import threading 3 | from concurrent.futures import Future 4 | 5 | def run_async(coro): 6 | """ 7 | 在新的事件循环中运行异步任务并返回结果。 8 | 9 | 参数: 10 | coro: 协程对象 11 | 12 | 返回: 13 | 协程的结果 14 | """ 15 | # 如果是已经在一个事件循环中,尝试另起一个线程,在该线程中建新的事件循环来执行协程(一个线程里只能有一个事件循环) 16 | future = Future() 17 | 18 | def runner(coro, future): 19 | try: 20 | loop = asyncio.new_event_loop() 21 | asyncio.set_event_loop(loop) 22 | result = loop.run_until_complete(coro) 23 | future.set_result(result) 24 | except Exception as e: 25 | future.set_exception(e) 26 | finally: 27 | loop.close() 28 | 29 | thread = threading.Thread(target=runner, args=(coro, future)) 30 | thread.start() 31 | thread.join() 32 | 33 | return future.result() -------------------------------------------------------------------------------- /Agently/Workflow/utils/runtime_supports.py: -------------------------------------------------------------------------------- 1 | from ..lib.constants import DEFAULT_INPUT_HANDLE_VALUE, DEFAULT_OUTPUT_HANDLE_VALUE 2 | from .find import find_by_attr 3 | from ..Runtime.BranchState import RuntimeBranchState 4 | 5 | def get_default_input_val(inputs_with_handle_name: dict, default_val = None): 6 | if DEFAULT_INPUT_HANDLE_VALUE in inputs_with_handle_name: 7 | return inputs_with_handle_name[DEFAULT_INPUT_HANDLE_VALUE] or default_val 8 | for hanldle in inputs_with_handle_name: 9 | return inputs_with_handle_name[hanldle] or default_val 10 | return default_val 11 | 12 | def get_default_handle(handles, handle_type = 'inputs'): 13 | if not handles: 14 | return None 15 | default_handle_name = DEFAULT_INPUT_HANDLE_VALUE if handle_type == 'inputs' else DEFAULT_OUTPUT_HANDLE_VALUE 16 | handle_list = handles[handle_type] or [] 17 | default_handle = find_by_attr(handle_list, 'handle', default_handle_name) 18 | if default_handle: 19 | return default_handle 20 | elif len(handle_list) > 0: 21 | return handle_list[0] 22 | return None 23 | 24 | 25 | def get_next_chunk_from_branch_queue(branch_state: RuntimeBranchState): 26 | """获取队列里下一个该执行的chunk""" 27 | if len(branch_state.running_queue): 28 | return branch_state.running_queue[0] 29 | if len(branch_state.slow_queue): 30 | return branch_state.slow_queue[0] 31 | return None 32 | -------------------------------------------------------------------------------- /Agently/Workflow/utils/verify.py: -------------------------------------------------------------------------------- 1 | def validate_dict(input_dict, required_keys = []): 2 | # 检查是否包含所有必需的键 3 | for key in required_keys: 4 | if key not in input_dict: 5 | return {'status': False, 'key': key } 6 | 7 | return { 'status': True } -------------------------------------------------------------------------------- /Agently/Workflow/yamlflow/__init__.py: -------------------------------------------------------------------------------- 1 | from .yamlflow import start_yaml_from_str, start_yaml_from_path -------------------------------------------------------------------------------- /Agently/Workflow/yamlflow/preset_chunks/Print.py: -------------------------------------------------------------------------------- 1 | def create(workflow, chunk_id, chunk_info): 2 | @workflow.chunk(chunk_id=chunk_id, **chunk_info) 3 | def print_executor(inputs, storage): 4 | if len(inputs.keys()) == 1 and "input" in inputs: 5 | print( 6 | chunk_info["placeholder"] if "placeholder" in chunk_info else ">>>", 7 | inputs["input"] 8 | ) 9 | else: 10 | print( 11 | chunk_info["placeholder"] if "placeholder" in chunk_info else ">>>", 12 | inputs 13 | ) -------------------------------------------------------------------------------- /Agently/Workflow/yamlflow/preset_chunks/Start.py: -------------------------------------------------------------------------------- 1 | def create(workflow, chunk_id, chunk_info): 2 | workflow.chunk(chunk_id=chunk_id, type="Start")(lambda:None) -------------------------------------------------------------------------------- /Agently/Workflow/yamlflow/preset_chunks/UserInput.py: -------------------------------------------------------------------------------- 1 | def create(workflow, chunk_id, chunk_info): 2 | @workflow.chunk(chunk_id=chunk_id, **chunk_info) 3 | def user_input_executor(inputs, storage): 4 | input_str = input( 5 | chunk_info["placeholder"] if "placeholder" in chunk_info else "[User]:", 6 | ) 7 | return input_str -------------------------------------------------------------------------------- /Agently/Workflow/yamlflow/preset_chunks/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | 4 | def scan(): 5 | type_list = [] 6 | create_dict = {} 7 | dir_path = os.path.dirname(os.path.abspath(__file__)) 8 | for item in os.listdir(dir_path): 9 | if item.endswith(".py") and item != "__init__.py": 10 | chunk_type_name = item[:-3] 11 | type_list.append(chunk_type_name) 12 | preset_chunk = importlib.import_module(f".{ chunk_type_name }", package = __package__) 13 | if not hasattr(preset_chunk, "create"): 14 | raise Exception(f"Preset chunk '{ chunk_type_name }' must state a `create()` function.") 15 | create_dict.update({ chunk_type_name: getattr(preset_chunk, "create") }) 16 | return { 17 | "type_list": type_list, 18 | "create": create_dict, 19 | } 20 | 21 | preset_chunks = scan() -------------------------------------------------------------------------------- /Agently/__init__.py: -------------------------------------------------------------------------------- 1 | #import nest_asyncio 2 | try: 3 | import readline 4 | except ImportError: 5 | import pyreadline as readline 6 | from .Request import Request 7 | from .Agent import AgentFactory 8 | from .Facility import FacilityManager 9 | from .Workflow import Workflow, Schema as WorkflowSchema, Checkpoint as WorkflowCheckpoint 10 | from .AppConnector import AppConnector 11 | from .FastServer import FastServer 12 | from ._global import global_plugin_manager, global_settings, global_storage, global_tool_manager 13 | from .utils import * 14 | from .Stage import Stage, Tunnel, MessageCenter, EventEmitter 15 | 16 | #nest_asyncio.apply() 17 | def create_agent(*args, **kwargs): 18 | return AgentFactory().create_agent(*args, **kwargs) 19 | 20 | facility = FacilityManager() 21 | lib = facility 22 | 23 | set_settings = global_settings.set 24 | 25 | def set_compatible(compatible_version: str): 26 | if isinstance(compatible_version, str): 27 | compatible_version = int(compatible_version.replace(".", "")) 28 | set_settings("compatible_version", compatible_version) 29 | # auto switch settings by compatible version 30 | if compatible_version <= 3320: 31 | set_settings("storage_type", "FileStorage") 32 | set_settings("storage.FileStorage.path", "./.Agently") 33 | 34 | def register_plugin(module_name:str, plugin_name: str, plugin: callable): 35 | global_plugin_manager.register(module_name, plugin_name, plugin) 36 | facility.refresh_plugins() 37 | 38 | def set_plugin_settings(module_name: str, plugin_name: str, key: str, value: any): 39 | global_plugin_manager.set_settings(f"plugin_settings.{ module_name }.{ plugin_name }", key, value) 40 | 41 | def attach_workflow(name: str, workflow: object): 42 | class AttachedWorkflow: 43 | def __init__(self, agent: object): 44 | self.agent = agent 45 | self.get_debug_status = lambda: self.agent.settings.get_trace_back("is_debug") 46 | self.settings = RuntimeCtxNamespace(f"plugin_settings.agent_component.{ name }", self.agent.settings) 47 | 48 | def start_workflow(self, init_inputs: dict=None, init_storage: dict={}): 49 | if not isinstance(init_storage, dict): 50 | raise Exception("[Workflow] Initial storage must be a dict.") 51 | init_storage.update({ "$agent": self.agent }) 52 | return workflow.start(init_inputs, storage=init_storage) 53 | 54 | def export(self): 55 | return { 56 | "alias": { 57 | name: { "func": self.start_workflow, "return_value": True }, 58 | } 59 | } 60 | return register_plugin("agent_component", name, AttachedWorkflow) -------------------------------------------------------------------------------- /Agently/_global.py: -------------------------------------------------------------------------------- 1 | from .utils import PluginManager, StorageDelegate, ToolManager, RuntimeCtx 2 | from .plugins import install_plugins 3 | 4 | global_plugin_manager = PluginManager() 5 | global_tool_manager = ToolManager() 6 | global_settings = RuntimeCtx() 7 | install_plugins(global_plugin_manager, global_tool_manager, global_settings) 8 | global_storage = StorageDelegate( 9 | db_name = "global", 10 | plugin_manager = global_plugin_manager, 11 | settings = global_settings, 12 | ) -------------------------------------------------------------------------------- /Agently/global_plugin_manager.py: -------------------------------------------------------------------------------- 1 | from .utils import PluginManager 2 | from .plugins import install_plugins 3 | 4 | global_plugin_manager = PluginManager() 5 | install_plugins(global_plugin_manager) -------------------------------------------------------------------------------- /Agently/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import importlib 5 | from Agently.utils import find_json 6 | 7 | def install_plugins(plugin_manager, tool_manager, settings): 8 | dir_path = os.path.dirname(os.path.abspath(__file__)) 9 | for item in os.listdir(dir_path): 10 | if os.path.exists(f"{ dir_path }/{ item }/__init__.py"): 11 | module_plugins = importlib.import_module(f".{ item }", package = __package__) 12 | if not hasattr(module_plugins, "export"): 13 | raise Exception(f"[Plugin Manager] Function 'export' must be stated in '__init__.py' file in module plugin dir: { dir_path }/{ item }") 14 | module_plugin_export = getattr(module_plugins, "export")() 15 | module_plugin_list = module_plugin_export[0] 16 | module_default_settings = module_plugin_export[1] 17 | orders = module_plugin_export[2] if len(module_plugin_export) > 2 else None 18 | try: 19 | for plugin_info in module_plugin_list: 20 | plugin_manager.register(plugin_info[0], plugin_info[1], plugin_info[2]) 21 | except: 22 | raise Exception(f"[Plugin Manager] Function 'export' in '__init__.py' must return a list of tuple (, , ).") 23 | if module_default_settings: 24 | #plugin_manager.update_settings(module_default_settings) 25 | for key, value in module_default_settings.items(): 26 | if isinstance(value, str) and value.lower() == "true": 27 | module_default_settings[key] = True 28 | if isinstance(value, str) and value.lower() == "false": 29 | module_default_settings[key] = False 30 | json_string = find_json(value) 31 | if json_string != None and json_string != '': 32 | module_default_settings[key] = json.loads(json_string) 33 | settings.set(key, module_default_settings[key]) 34 | if orders and item == "agent_component": 35 | agent_component_list = plugin_manager.get_agent_component_list() 36 | for process_type in orders: 37 | order_list = orders[process_type].split(",") 38 | current_mode = "firstly" 39 | order_settings = { 40 | "firstly": [], 41 | "normally": [], 42 | "finally": [], 43 | } 44 | for agent_component_name in order_list: 45 | agent_component_name = agent_component_name.replace(" ", "") 46 | if agent_component_name == "...": 47 | current_mode = "finally" 48 | elif agent_component_name in agent_component_list: 49 | order_settings[current_mode].append(agent_component_name) 50 | ordered_agent_component_list = order_settings["firstly"].copy() 51 | ordered_agent_component_list.extend(order_settings["finally"].copy()) 52 | for agent_component_name in agent_component_list: 53 | if agent_component_name not in ordered_agent_component_list: 54 | order_settings["normally"].append(agent_component_name) 55 | settings.set(f"plugin_settings.agent_component.orders.{ process_type }", order_settings) 56 | for tool_package_name, ToolPackageClass in plugin_manager.plugins_runtime_ctx.get("tool", {}).items(): 57 | tool_package = ToolPackageClass(tool_manager) 58 | for tool_name, tool_info in tool_package.export().items(): 59 | if "desc" not in tool_info: 60 | raise Exception(f"[Plugin] Tool '{ tool_name }' in tool package '{ tool_package_name }' must export key 'desc'.") 61 | if "args" not in tool_info or not isinstance(tool_info["args"], dict): 62 | raise Exception(f"[Plugin] Tool '{ tool_name }' in tool package '{ tool_package_name }' must export key 'args', set the value as {{}} if need no args.") 63 | if "func" not in tool_info or not callable(tool_info["func"]): 64 | raise Exception(f"[Plugin] Tool '{ tool_name }' in tool package '{ tool_package_name }' must export key 'func'.") 65 | tool_manager.register( 66 | tool_name = tool_name, 67 | desc = tool_info["desc"], 68 | args = tool_info["args"], 69 | func = tool_info["func"], 70 | require_proxy = tool_info["require_proxy"] if "require_proxy" in tool_info else False, 71 | categories = tool_info["categories"] if "categories" in tool_info else None 72 | ) 73 | 74 | def install(Agently): 75 | install_plugins(Agently.global_plugin_manager, Agently.global_tool_manager, Agently.global_settings) 76 | Agently.facility.refresh_plugins() 77 | 78 | def install_to_agent(agent): 79 | install_plugins(agent.plugin_manager, agent.tool_manager, agent.settings) 80 | agent.refresh_plugins() -------------------------------------------------------------------------------- /Agently/plugins/agent_component/Decorator.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import ast 3 | from .utils import ComponentABC 4 | 5 | class Decorator(ComponentABC): 6 | def __init__(self, agent: object): 7 | self.agent = agent 8 | 9 | def auto_func(self, func: callable): 10 | def wrapper(*args, **kwargs): 11 | # generate input dict 12 | signature = inspect.signature(func) 13 | arguments = signature.bind(*args, **kwargs) 14 | arguments.apply_defaults() 15 | input_dict = {} 16 | for param in signature.parameters: 17 | input_dict.update({ param: arguments.arguments[param] }) 18 | # generate instruction 19 | instruction = inspect.getdoc(func) 20 | # generate output dict 21 | output_dict = signature.return_annotation 22 | return ( 23 | self.agent 24 | .input(input_dict) 25 | .instruct(instruction) 26 | .output(output_dict) 27 | .start() 28 | ) 29 | return wrapper 30 | 31 | def on_event(self, event: str=None, *, is_await:bool=False): 32 | if event is None: 33 | event = "*" 34 | if ( 35 | not event.startswith("response:") 36 | and not event.startswith("tool:") 37 | and not event.startswith("realtime:") 38 | and not event == "realtime" 39 | and not event.startswith("instant:") 40 | and not event == "instant" 41 | and not event == "*" 42 | ): 43 | event = "response:" + event 44 | def decorator(func: callable): 45 | self.agent.add_event_listener(event, func, is_await=is_await) 46 | return decorator 47 | 48 | def register_tool(self, **tool_info_kwrags): 49 | def decorator(func: callable): 50 | # get tool name 51 | if "tool_name" not in tool_info_kwrags: 52 | tool_info_kwrags.update({ "tool_name": func.__name__ }) 53 | # get desc 54 | if "desc" not in tool_info_kwrags: 55 | tool_info_kwrags.update({ "desc": inspect.getdoc(func) }) 56 | # get args 57 | if "args" not in tool_info_kwrags: 58 | func_ast = ast.parse(inspect.getsource(func)) 59 | tool_info_kwrags.update({ "args": {} }) 60 | for node in func_ast.body[0].args.args: 61 | if node.arg != "self": 62 | tool_info_kwrags["args"].update({ 63 | node.arg: 64 | (node.annotation.dims[0].value, node.annotation.dims[1].value) 65 | if isinstance(node.annotation, ast.Tuple) 66 | else (type(node.annotation.value).__name__, node.annotation.value) 67 | }) 68 | # get func 69 | tool_info_kwrags.update({ "func": func }) 70 | self.agent.register_tool(**tool_info_kwrags) 71 | return func 72 | return decorator 73 | 74 | def export(self): 75 | return { 76 | "prefix": None, 77 | "suffix": None, 78 | "alias": { 79 | "auto_func": { "func": self.auto_func, "return_value": True }, 80 | "on_event": { "func": self.on_event, "return_value": True }, 81 | "tool": { "func": self.register_tool, "return_value": True }, 82 | }, 83 | } 84 | 85 | def export(): 86 | return ("Decorator", Decorator) -------------------------------------------------------------------------------- /Agently/plugins/agent_component/ReplyReformer.py: -------------------------------------------------------------------------------- 1 | from .utils import ComponentABC 2 | 3 | class ReplyReformer(ComponentABC): 4 | def __init__(self, agent: object): 5 | self.agent = agent 6 | self.reform_handler = None 7 | 8 | def add_reply_reform_handler(self, reform_handler: callable): 9 | self.reform_handler = reform_handler 10 | return self.agent 11 | 12 | def _suffix(self, event, data): 13 | if event == "response:finally" and callable(self.reform_handler): 14 | self.agent.request.response_cache["reply"] = self.reform_handler(data) 15 | 16 | def export(self): 17 | return { 18 | "prefix": None, 19 | "suffix": self._suffix, 20 | "alias": { 21 | "reform_reply": { "func": self.add_reply_reform_handler } 22 | }, 23 | } 24 | 25 | def export(): 26 | return ("ReplyReformer", ReplyReformer) -------------------------------------------------------------------------------- /Agently/plugins/agent_component/Role.py: -------------------------------------------------------------------------------- 1 | from .utils import ComponentABC 2 | from Agently.utils import RuntimeCtxNamespace 3 | 4 | class Role(ComponentABC): 5 | def __init__(self, agent: object): 6 | self.agent = agent 7 | self.role_runtime_ctx = RuntimeCtxNamespace("role", self.agent.agent_runtime_ctx) 8 | self.role_storage = self.agent.global_storage.table("role") 9 | 10 | def set_id(self, role_id: str): 11 | self.role_runtime_ctx.set("$ID", role_id) 12 | return self.agent 13 | 14 | def set(self, key: any, value: any=None): 15 | if value is not None: 16 | self.role_runtime_ctx.set(key, value) 17 | else: 18 | self.role_runtime_ctx.set("DESC", key) 19 | return self.agent 20 | 21 | def update(self, key: any, value: any=None): 22 | if value is not None: 23 | self.role_runtime_ctx.update(key, value) 24 | else: 25 | self.role_runtime_ctx.update("DESC", key) 26 | return self.agent 27 | 28 | def append(self, key: any, value: any=None): 29 | if value is not None: 30 | self.role_runtime_ctx.append(key, value) 31 | else: 32 | self.role_runtime_ctx.append("DESC", key) 33 | return self.agent 34 | 35 | def extend(self, key: any, value: any=None): 36 | if value is not None: 37 | self.role_runtime_ctx.extend(key, value) 38 | else: 39 | self.role_runtime_ctx.extend("DESC", key) 40 | return self.agent 41 | 42 | def save(self, role_id: str=None): 43 | role_id = role_id or self.role_runtime_ctx.get("$ID") 44 | if role_id is None or role_id == "": 45 | raise Exception("[Agent Component: Role] Role ID must be stated before save. Use .set_role_id() to specific it or pass role id into .save_role().") 46 | self.role_storage.set(role_id, self.role_runtime_ctx.get()).save() 47 | return self.agent 48 | 49 | def load(self, role_id: str): 50 | role_id = role_id or self.role_runtime_ctx.get("$ID") 51 | if role_id is None or role_id == "": 52 | raise Exception("[Agent Component: Role] Role ID must be stated before load. Use .set_role_id() to specific it or pass role id into .load_role().") 53 | role_data = self.role_storage.get(role_id) or {} 54 | for key, value in role_data.items(): 55 | self.role_runtime_ctx.update(key, value) 56 | return self.agent 57 | 58 | def _prefix(self): 59 | role_settings = self.role_runtime_ctx.get() or {} 60 | if "$ID" in role_settings: 61 | del role_settings["$ID"] 62 | return { 63 | "role": role_settings, 64 | } 65 | 66 | def export(self): 67 | return { 68 | "prefix": self._prefix, 69 | "suffix": None, 70 | "alias": { 71 | "set_role_id": { "func": self.set_id }, 72 | "set_role": { "func": self.set }, 73 | "update_role": { "func": self.update }, 74 | "append_role": { "func": self.append }, 75 | "extend_role": { "func": self.extend }, 76 | "save_role": { "func": self.save }, 77 | "load_role": { "func": self.load }, 78 | }, 79 | } 80 | 81 | def export(): 82 | return ("Role", Role) -------------------------------------------------------------------------------- /Agently/plugins/agent_component/Search.py: -------------------------------------------------------------------------------- 1 | from .utils import ComponentABC 2 | from Agently.utils import RuntimeCtxNamespace 3 | from duckduckgo_search import DDGS 4 | 5 | class Search(ComponentABC): 6 | def __init__(self, agent): 7 | self.agent = agent 8 | self.is_debug = self.agent.settings.get_trace_back("is_debug") 9 | self.settings = RuntimeCtxNamespace("plugin_settings.agent_component.Search", self.agent.settings) 10 | 11 | def __exec_search(self, keywords: str, options:dict={}): 12 | result = [] 13 | search_kwargs = {} 14 | proxy = self.agent.settings.get_trace_back("proxy") 15 | if proxy: 16 | search_kwargs["proxies"] = proxy 17 | #else: 18 | #if self.is_debug: 19 | #print("[No Proxy for Search] That may cause http request error in some areas. If request error occured, please use .set_proxy('') or .set_settings('proxy', '') to set a proxy.") 20 | if "max_results" not in options: 21 | options.update({ "max_results": self.settings.get_trace_back("max_result_count", 5) }) 22 | with DDGS(**search_kwargs) as ddgs: 23 | for r in ddgs.text( 24 | keywords, 25 | **options 26 | ): 27 | result.append(r) 28 | return result 29 | 30 | def use_search(self): 31 | self.agent.toggle_component("Search", True) 32 | return self.agent 33 | 34 | def set_max_results(self, max_result_count: int): 35 | self.settings.set("max_result_count", max_result_count) 36 | return self.agent 37 | 38 | def set_max_result_length(self, max_result_length: int): 39 | self.settings.set("max_result_length", max_result_length) 40 | return self.agent 41 | 42 | def switch_mode(self, mode: int): 43 | if mode in (0, 1): 44 | self.mode = mode 45 | return self.agent 46 | 47 | def _prefix(self): 48 | request = self.agent.worker_request 49 | if self.is_debug: 50 | print("[Agent Component] Searching: Start search judgement...") 51 | result = request\ 52 | .input(self.agent.request.request_runtime_ctx.get("prompt")["input"])\ 53 | .output({ 54 | "certain_of_direct_answer": ("Boolean", "if you are cretain that you can answer {input} directly, return true, else false."), 55 | "keywords": ( 56 | "String", 57 | "- split keywords by space.\n- if do not have one remains {keywords} as ""\n- if you want to search a definition, use the format '{keywords}' to make sure the exact search\n - if you want to search multiple times, split each search by '|'" 58 | ), 59 | })\ 60 | .start() 61 | if not result["certain_of_direct_answer"]: 62 | search_result_dict = {} 63 | max_result_length = self.settings.get("max_result_length", 1000) 64 | result_length = 0 65 | keywords_list = result["keywords"].split("|") 66 | for keywords in keywords_list: 67 | if self.is_debug: 68 | print(f"[Agent Component] Searching: { keywords }") 69 | search_result_dict.update({ keywords: [] }) 70 | for search_result in self.__exec_search(keywords): 71 | search_result = f"{ search_result['title'] }: { search_result['body'] }" 72 | search_result_dict[keywords].append(search_result) 73 | result_length += len(search_result) 74 | if result_length > max_result_length: 75 | break 76 | if self.is_debug: 77 | print(f"[Agent Component] Search Result: { keywords }\n{ search_result_dict[keywords] }") 78 | if len(search_result_dict.keys()) > 0: 79 | return { "information": { "search results": search_result_dict }, "instruction": "answer according {information}" } 80 | else: 81 | return None 82 | else: 83 | return None 84 | 85 | def export(self): 86 | return { 87 | "prefix": self._prefix, 88 | "suffix": None, 89 | "alias": { 90 | "use_search": { "func": self.use_search }, 91 | }, 92 | } 93 | 94 | def export(): 95 | return ("Search", Search) -------------------------------------------------------------------------------- /Agently/plugins/agent_component/Status.py: -------------------------------------------------------------------------------- 1 | from .utils import ComponentABC 2 | from Agently.utils import RuntimeCtxNamespace 3 | 4 | class Status(ComponentABC): 5 | def __init__(self, agent: object): 6 | self.agent = agent 7 | self.status_runtime_ctx = RuntimeCtxNamespace("status", self.agent.agent_runtime_ctx) 8 | self.status_mapping_runtime_ctx = RuntimeCtxNamespace("status_mapping", self.agent.agent_runtime_ctx) 9 | self.status_storage = self.agent.global_storage.table("status") 10 | self.settings = RuntimeCtxNamespace("plugin_settings.agent_component.Status", self.agent.settings) 11 | self.global_status_namespace = None 12 | 13 | # Defer loading to avoid unnecessary actions in initialization 14 | if self.settings.get_trace_back("auto"): 15 | self.load() 16 | 17 | def set(self, key: str, value: str): 18 | self.status_runtime_ctx.set(key, value) 19 | return self.agent 20 | 21 | def use_global_status(self, namespace_name: str="default"): 22 | self.global_status_namespace = namespace_name 23 | return self.agent 24 | 25 | def append_mapping(self, status_key: str, status_value: str, alias_name: str, *args, **kwargs): 26 | self.status_mapping_runtime_ctx.append( 27 | f"{status_key}.{status_value}", 28 | { 29 | "alias_name": alias_name, 30 | "args": args, 31 | "kwargs": kwargs, 32 | } 33 | ) 34 | return self.agent 35 | 36 | def save(self): 37 | status = self.status_runtime_ctx.get() 38 | self.status_storage.set(status).save() 39 | return self.agent 40 | 41 | def load(self): 42 | status = self.status_storage.get() 43 | self.status_runtime_ctx.update(status) 44 | return self.agent 45 | 46 | def _apply_mappings(self, mappings_dict, status_key, status_value): 47 | """Helper function to apply mappings to the agent.""" 48 | if status_key in mappings_dict and status_value in mappings_dict[status_key]: 49 | for mapping in mappings_dict[status_key][status_value]: 50 | alias_func = getattr(self.agent, mapping["alias_name"], None) 51 | if alias_func: 52 | alias_func(*mapping["args"], **mapping["kwargs"]) 53 | 54 | def _prefix(self): 55 | agent_status = self.status_runtime_ctx.get() 56 | if not agent_status: 57 | return None 58 | 59 | # Load global mappings if the namespace is set 60 | global_mappings = {} 61 | if self.global_status_namespace: 62 | global_mappings = self.agent.global_storage.table(f"status_mapping.{self.global_status_namespace}").get() or {} 63 | 64 | agent_mappings = self.status_mapping_runtime_ctx.get() or {} 65 | 66 | for status_key, status_value in agent_status.items(): 67 | self._apply_mappings(global_mappings, status_key, status_value) 68 | self._apply_mappings(agent_mappings, status_key, status_value) 69 | 70 | return None 71 | 72 | def export(self): 73 | return { 74 | "prefix": self._prefix, 75 | "suffix": None, 76 | "alias": { 77 | "set_status": {"func": self.set}, 78 | "save_status": {"func": self.save}, 79 | "load_status": {"func": self.load}, 80 | "use_global_status": {"func": self.use_global_status}, 81 | "append_status_mapping": {"func": self.append_mapping}, 82 | }, 83 | } 84 | 85 | def export(): 86 | return ("Status", Status) 87 | -------------------------------------------------------------------------------- /Agently/plugins/agent_component/UserInfo.py: -------------------------------------------------------------------------------- 1 | from .utils import ComponentABC 2 | from Agently.utils import RuntimeCtxNamespace 3 | 4 | class UserInfo(ComponentABC): 5 | def __init__(self, agent: object): 6 | self.agent = agent 7 | self.user_info_runtime_ctx = RuntimeCtxNamespace("user_info", self.agent.agent_runtime_ctx) 8 | self.user_info_storage = self.agent.global_storage.table("user_info") 9 | 10 | def set_name(self, name: str): 11 | self.user_info_runtime_ctx = self.__get_user_info_runtime_ctx(target) 12 | self.user_info_runtime_ctx.set("NAME", name) 13 | return self.agent 14 | 15 | def set(self, key: any, value: any=None): 16 | if value is not None: 17 | self.user_info_runtime_ctx.set(key, value) 18 | else: 19 | self.user_info_runtime_ctx.set("DESC", key) 20 | return self.agent 21 | 22 | def update(self, key: any, value: any=None): 23 | if value is not None: 24 | self.user_info_runtime_ctx.update(key, value) 25 | else: 26 | self.user_info_runtime_ctx.update("DESC", key) 27 | return self.agent 28 | 29 | def append(self, key: any, value: any=None): 30 | if value is not None: 31 | self.user_info_runtime_ctx.append(key, value) 32 | else: 33 | self.user_info_runtime_ctx.append("DESC", key) 34 | return self.agent 35 | 36 | def extend(self, key: any, value: any=None): 37 | if value is not None: 38 | self.user_info_runtime_ctx.extend(key, value) 39 | else: 40 | self.user_info_runtime_ctx.extend("DESC", key) 41 | return self.agent 42 | 43 | def save(self, role_name: str=None): 44 | if user_info_name == None: 45 | user_info_name = self.user_info_runtime_ctx.get("NAME") 46 | if user_info_name != None and user_info_name != "": 47 | self.user_info_storage\ 48 | .set(user_info_name, self.user_info_runtime_ctx.get())\ 49 | .save() 50 | return self.agent 51 | else: 52 | raise Exception("[Agent Component: UserInfo] UserInfo attr 'NAME' must be stated before save. Use .set_user_name() to specific that.") 53 | 54 | def load(self, role_name: str): 55 | user_info_data = self.user_info_storage.get(role_name) 56 | for key, value in user_info_data.items(): 57 | self.user_info_runtime_ctx.update(key, value) 58 | return self.agent 59 | 60 | def _prefix(self): 61 | return { 62 | "user_info": self.user_info_runtime_ctx.get(), 63 | } 64 | 65 | def export(self): 66 | return { 67 | "prefix": self._prefix, 68 | "suffix": None, 69 | "alias": { 70 | "set_user_name": { "func": self.set_name }, 71 | "set_user_info": { "func": self.set }, 72 | "update_user_info": { "func": self.update }, 73 | "append_user_info": { "func": self.append }, 74 | "extend_user_info": { "func": self.extend }, 75 | "save_user_info": { "func": self.save }, 76 | "load_user_info": { "func": self.load }, 77 | }, 78 | } 79 | 80 | def export(): 81 | return ("UserInfo", UserInfo) -------------------------------------------------------------------------------- /Agently/plugins/agent_component/YAMLLoader.py: -------------------------------------------------------------------------------- 1 | import yaml as YAML 2 | from .utils import ComponentABC 3 | from Agently.utils import RuntimeCtxNamespace 4 | 5 | class YAMLLoader(ComponentABC): 6 | def __init__(self, agent: object): 7 | self.agent = agent 8 | self.is_debug = lambda: self.agent.settings.get_trace_back("is_debug") 9 | self.settings = RuntimeCtxNamespace("plugin_settings.agent_component.YAMLReader", self.agent.settings) 10 | 11 | def transform_to_agently_style(self, target: any, variables: dict={}): 12 | if isinstance(target, dict): 13 | result = {} 14 | agently_tuple_list = [None, None] 15 | for key, value in target.items(): 16 | if key == "$type": 17 | agently_tuple_list[0] = self.transform_to_agently_style(value, variables=variables) 18 | elif key == "$desc": 19 | agently_tuple_list[1] = self.transform_to_agently_style(value, variables=variables) 20 | else: 21 | result.update({ key: self.transform_to_agently_style(value, variables=variables) }) 22 | if agently_tuple_list[0] or agently_tuple_list[1]: 23 | result = (agently_tuple_list[0], agently_tuple_list[1]) 24 | return result 25 | if isinstance(target, list): 26 | result = [] 27 | for item in target: 28 | result.append(self.transform_to_agently_style(item, variables=variables)) 29 | return result 30 | else: 31 | result = str(target) 32 | global_variables = self.agent.settings.get("global_variables") 33 | if global_variables: 34 | for key, value in global_variables.items(): 35 | result = result.replace("${" + str(key) + "}", str(value)) 36 | for key, value in variables.items(): 37 | result = result.replace("${" + str(key) + "}", str(value)) 38 | return result 39 | 40 | def load_yaml_prompt(self, *, path:str=None, yaml:str=None, use_agently_style:bool=True, variables:dict={}): 41 | # load yaml content 42 | yaml_dict = {} 43 | if variables: 44 | use_agently_style = True 45 | if not path and not yaml: 46 | raise Exception(f"[Agent Component: YAMLReader]: one parameter between `path` or `yaml` must be provided.") 47 | try: 48 | if path: 49 | with open(path, "r", encoding="utf-8") as yaml_file: 50 | yaml_dict = YAML.safe_load(yaml_file) 51 | if use_agently_style: 52 | yaml_dict = self.transform_to_agently_style(yaml_dict, variables=variables) 53 | else: 54 | yaml_dict = YAML.safe_load(yaml) 55 | if use_agently_style: 56 | yaml_dict = self.transform_to_agently_style(yaml_dict, variables=variables) 57 | except Exception as e: 58 | raise Exception(f"[Agent Component: YAMLReader]: Error occured when read YAML from path '{ path }'.\nError: { str(e) }") 59 | # run agent alias 60 | agent_alias_list = dir(self.agent) 61 | for alias, value in yaml_dict.items(): 62 | if alias in agent_alias_list: 63 | args = [] 64 | kwargs = {} 65 | if isinstance(value, dict) and ("$args" in value or "$kwargs" in value): 66 | if "$args" in value and isinstance(value["$args"], list): 67 | args = value["$args"].copy() 68 | if "$kwargs" in value and isinstance(value["$kwargs"], dict): 69 | kwargs = value["$kwargs"].copy() 70 | getattr(self.agent, alias)(*args, **kwargs) 71 | else: 72 | getattr(self.agent, alias)(value) 73 | return self.agent 74 | 75 | def export(self): 76 | return { 77 | "alias": { 78 | "load_yaml_prompt": { "func": self.load_yaml_prompt }, 79 | } 80 | } 81 | 82 | def export(): 83 | return ("YAMLLoader", YAMLLoader) -------------------------------------------------------------------------------- /Agently/plugins/agent_component/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | import configparser 4 | 5 | def export(): 6 | plugin_list = [] 7 | 8 | dir_path = os.path.dirname(os.path.abspath(__file__)) 9 | dir_name = os.path.basename(dir_path) 10 | 11 | # read config.ini 12 | config = configparser.ConfigParser() 13 | config.optionxform = str 14 | config.read(f"{ dir_path }/config.ini") 15 | config_is_not_empty = len(dict(config)) > 1 16 | 17 | # load configs 18 | module_name = config["plugins"]["module_name"] if config_is_not_empty and "module_name" in config else dir_name 19 | exception_files = set(config["plugins"]["exception_files"]) if config_is_not_empty and "exception_files" in config["plugins"] else set([]) 20 | exception_files.add("__init__.py") 21 | exception_dirs = set(config["plugins"]["exception_dirs"]) if config_is_not_empty and "exception_dirs" in config["plugins"] else set([]) 22 | exception_dirs.add("utils") 23 | default_settings = dict(config["default settings"]) if config_is_not_empty and "default settings" in config else None 24 | orders = dict(config["orders"]) if config_is_not_empty and "orders" in config else None 25 | 26 | for item in os.listdir(dir_path): 27 | # import plugins in .py files 28 | if item.endswith('.py') and item not in exception_files: 29 | plugin = importlib.import_module(f".{ item[:-3] }", package = __package__) 30 | if not hasattr(plugin, "export"): 31 | raise Exception(f"[Plugin Manager] Function 'export' must be stated in plugin file: { dir_path }/{ item }") 32 | plugin_export = getattr(plugin, "export")() 33 | plugin_list.append((module_name, plugin_export[0], plugin_export[1])) 34 | # import plugins in dirs 35 | if os.path.exists(f"{ dir_path }/{ item }/__init__.py") and item not in exception_dirs: 36 | plugin = importlib.import_module(f".{ item }", package = __package__) 37 | if not hasattr(plugin, "export"): 38 | raise Exception(f"[Plugin Manager] Function 'export' must be stated in plugin dir's '__init__.py' file: { dir_path }/{ item }/__init__.py") 39 | plugin_export = getattr(plugin, "export")() 40 | plugin_list.append((module_name, plugin_export[0], plugin_export[1])) 41 | 42 | return (plugin_list, default_settings, orders) -------------------------------------------------------------------------------- /Agently/plugins/agent_component/config.ini: -------------------------------------------------------------------------------- 1 | [plugins] 2 | module_name = agent_component 3 | exception_files = __init__.py 4 | exception_dirs = utils 5 | 6 | [default settings] 7 | component_toggles.Role = True 8 | component_toggles.Status = True 9 | component_toggles.OpenAIAssistant = True 10 | component_toggles.Segment = True 11 | component_toggles.Search = False 12 | plugin_settings.agent_component.Session = { "auto_save": true, "max_length": 12000, "strict_orders": true, "manual_history": false } 13 | 14 | [orders] 15 | prefix = EventListener, Status, ..., Search, Tool, Segment 16 | suffix = ReplyReformer, ..., Session, EventListener -------------------------------------------------------------------------------- /Agently/plugins/agent_component/utils/ComponentABC.py: -------------------------------------------------------------------------------- 1 | #ABC = Abstract Base Class 2 | from abc import ABC, abstractmethod 3 | 4 | class ComponentABC(ABC): 5 | def __init__(self, agent): 6 | self.agent = agent 7 | 8 | @abstractmethod 9 | def export(self): 10 | return { 11 | "prefix": callable or [callable],#()->(request_namespace: str, update_dict: dict) 12 | "suffix": callable,#(event: str, data: any)->None 13 | "alias": { 14 | alias_name: { 15 | "func": callable, 16 | "return_value": bool,# optional, default=False 17 | }, 18 | # ... 19 | }, 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Agently/plugins/agent_component/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .ComponentABC import ComponentABC -------------------------------------------------------------------------------- /Agently/plugins/facility/Embedding.py: -------------------------------------------------------------------------------- 1 | from .utils import FacilityABC 2 | from Agently.utils import RuntimeCtx, RuntimeCtxNamespace 3 | 4 | class Embedding(FacilityABC): 5 | def __init__(self, *, storage: object, plugin_manager: object, settings: object): 6 | self.plugin_manager = plugin_manager 7 | self.settings = settings 8 | 9 | def OpenAI(self, text: str, options: dict={}): 10 | from openai import OpenAI 11 | import httpx 12 | settings = RuntimeCtxNamespace("embedding.OpenAI", self.settings) 13 | # Prepare Client Params 14 | client_params = {} 15 | base_url = settings.get_trace_back("url") 16 | if base_url: 17 | client_params.update({ "base_url": base_url }) 18 | proxy = self.settings.get_trace_back("proxy") 19 | if proxy: 20 | client_params.update({ "http_client": httpx.Client( proxies = proxy ) }) 21 | api_key = settings.get_trace_back("auth.api_key") 22 | if api_key: 23 | client_params.update({ "api_key": api_key }) 24 | else: 25 | raise Exception("[Embedding] OpenAI require api_key. use Agently.facility.set_settings('embedding.OpenAI.auth', { 'api_key': '' }) to set it.") 26 | if "model" not in options: 27 | options["model"] = "text-embedding-ada-002" 28 | # Create Client 29 | client = OpenAI(**client_params) 30 | # Request 31 | result = client.embeddings.create( 32 | input = text.replace("\n", " "), 33 | **options 34 | ) 35 | if result.data: 36 | return result.data[0].embedding 37 | else: 38 | raise Exception(f"[Embedding] OpenAI Error: { dict(result) }") 39 | 40 | def ERNIE(self, text_list: (str, list), options: dict={}): 41 | import erniebot 42 | settings = RuntimeCtxNamespace("embedding.ERNIE", self.settings) 43 | erniebot.api_type = "aistudio" 44 | access_token = settings.get_trace_back("auth.aistudio") 45 | if access_token: 46 | erniebot.access_token = access_token 47 | else: 48 | raise Exception("[Embedding] ERNIE require aistudio access token. use Agently.facility.set_settings('embedding.ERNIE.auth', { 'aistudio': '' }) to set it.") 49 | if "model" not in options: 50 | options["model"] = "ernie-text-embedding" 51 | if not isinstance(text_list, list): 52 | text_list = [text_list] 53 | result = erniebot.Embedding.create( 54 | input = text_list, 55 | **options 56 | ) 57 | result = dict(result) 58 | if result["rcode"] == 200: 59 | return result["rbody"]["data"] 60 | else: 61 | raise Exception(f"[Embedding] ERNIE Error: { result }") 62 | 63 | def ZhipuAI(self, text: str, options: dict={}): 64 | import zhipuai 65 | settings = RuntimeCtxNamespace("embedding.ZhipuAI", self.settings) 66 | api_key = settings.get_trace_back("auth.api_key") 67 | if api_key: 68 | zhipuai.api_key = api_key 69 | else: 70 | raise Exception("[Embedding] ZhipuAI require api_key. use Agently.facility.set_settings('embedding.ZhipuAI.auth', { 'api_key': '' }) to set it.") 71 | if "model" not in options: 72 | options["model"] = "text_embedding" 73 | result = zhipuai.model_api.invoke( 74 | prompt = text, 75 | **options 76 | ) 77 | if "data" in result: 78 | return result["data"]["embedding"] 79 | else: 80 | raise Exception(f"[Embedding] ZhipuAI Error: { result }") 81 | 82 | def Google(self, text: str, options: dict={}): 83 | import json 84 | import httpx 85 | settings = RuntimeCtxNamespace("embedding.Google", self.settings) 86 | api_key = settings.get_trace_back("auth.api_key") 87 | if api_key == None: 88 | raise Exception("[Embedding] Google require api_key. use Agently.facility.set_settings('embedding.google.auth', { 'api_key': '' }) to set it.") 89 | proxy = self.settings.get_trace_back("proxy") 90 | model = "embedding-001" 91 | if "model" in options: 92 | model = options["model"] 93 | del options["model"] 94 | request_params = { 95 | "data": json.dumps({ 96 | "content": { 97 | "parts": [{ "text": text }] 98 | }, 99 | **options 100 | }), 101 | "timeout": None, 102 | } 103 | result = httpx.post( 104 | f"https://generativelanguage.googleapis.com/v1/models/{ model }:embedContent?key={ api_key }", 105 | **request_params 106 | ) 107 | if result.status_code: 108 | return result.json()["embedding"]["values"] 109 | else: 110 | raise Exception(f"[Embedding] Google Error: { dict(result) }") 111 | 112 | def export(): 113 | return ("embedding", Embedding) -------------------------------------------------------------------------------- /Agently/plugins/facility/RoleManager.py: -------------------------------------------------------------------------------- 1 | from .utils import FacilityABC 2 | from Agently.utils import RuntimeCtx 3 | 4 | class RoleManager(FacilityABC): 5 | def __init__(self, *, storage: object, plugin_manager: object, settings: object): 6 | self.storage = storage.table("role") 7 | self.plugin_manager = plugin_manager 8 | self.role_runtime_ctx = RuntimeCtx() 9 | 10 | def set_id(self, role_id: str): 11 | self.role_runtime_ctx.set("$ID", role_id) 12 | return self 13 | 14 | def set(self, key: any, value: any=None): 15 | if value is not None: 16 | self.role_runtime_ctx.set(key, value) 17 | else: 18 | self.role_runtime_ctx.set("DESC", key) 19 | return self 20 | 21 | def update(self, key: any, value: any=None): 22 | if value is not None: 23 | self.role_runtime_ctx.update(key, value) 24 | else: 25 | self.role_runtime_ctx.update("DESC", key) 26 | return self 27 | 28 | def append(self, key: any, value: any=None): 29 | if value is not None: 30 | self.role_runtime_ctx.append(key, value) 31 | else: 32 | self.role_runtime_ctx.append("DESC", key) 33 | return self 34 | 35 | def extend(self, key: any, value: any=None): 36 | if value is not None: 37 | self.role_runtime_ctx.extend(key, value) 38 | else: 39 | self.role_runtime_ctx.extend("DESC", key) 40 | return self 41 | 42 | def save(self, role_id: str=None): 43 | role_id = role_id or self.role_runtime_ctx.get("$ID") 44 | if role_id is None or role_id == "": 45 | raise Exception("[Facility: RoleMananger] Role ID must be stated before save. Use .set_id() to specific it or pass role id into .save().") 46 | self.storage.set(role_id, self.role_runtime_ctx.get()).save() 47 | self.role_runtime_ctx.empty() 48 | return self 49 | 50 | def get(self, role_id: str): 51 | return self.storage.get(role_id) 52 | 53 | def export(): 54 | return ("role_manager", RoleManager) -------------------------------------------------------------------------------- /Agently/plugins/facility/StatusManager.py: -------------------------------------------------------------------------------- 1 | from .utils import FacilityABC 2 | from Agently.utils import RuntimeCtx 3 | 4 | class StatusManager(FacilityABC): 5 | def __init__(self, *, storage: object, plugin_manager: object, settings: object): 6 | self.storage = storage.table("status_mapping.default") 7 | self.plugin_manager = plugin_manager 8 | 9 | def set_status_namespace(self, namespace_name: str): 10 | self.storage = storage.table(f"status_mapping.{ namespace_name }") 11 | return 12 | 13 | def set_mappings(self, status_key: str, status_value: str, alias_list: list): 14 | self.storage.update(f"{status_key}.{status_value}", alias_list).save() 15 | return self 16 | 17 | def append_mapping(self, status_key: str, status_value: str, alias_name: str, *args, **kwargs): 18 | self.storage\ 19 | .append( 20 | f"{status_key}.{status_value}", 21 | { 22 | "alias_name": alias_name, 23 | "args": args, 24 | "kwargs": kwargs, 25 | } 26 | )\ 27 | .save() 28 | return self 29 | 30 | def get_mapping(self, status_key: str, status_value: str): 31 | return self.storage.get(f"{status_key}.{status_value}") 32 | 33 | def export(): 34 | return ("status_manager", StatusManager) -------------------------------------------------------------------------------- /Agently/plugins/facility/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | import configparser 4 | 5 | def export(): 6 | plugin_list = [] 7 | 8 | dir_path = os.path.dirname(os.path.abspath(__file__)) 9 | dir_name = os.path.basename(dir_path) 10 | 11 | # read config.ini 12 | config = configparser.ConfigParser() 13 | config.optionxform = str 14 | config.read(f"{ dir_path }/config.ini") 15 | config_is_not_empty = len(dict(config)) > 1 16 | 17 | # load configs 18 | module_name = config["plugins"]["module_name"] if config_is_not_empty and "module_name" in config else dir_name 19 | exception_files = set(config["plugins"]["exception_files"]) if config_is_not_empty and "exception_files" in config["plugins"] else set([]) 20 | exception_files.add("__init__.py") 21 | exception_dirs = set(config["plugins"]["exception_dirs"]) if config_is_not_empty and "exception_dirs" in config["plugins"] else set([]) 22 | exception_dirs.add("utils") 23 | default_settings = dict(config["default settings"]) if config_is_not_empty and "default settings" in config else None 24 | 25 | for item in os.listdir(dir_path): 26 | # import plugins in .py files 27 | if item.endswith('.py') and item not in exception_files: 28 | plugin = importlib.import_module(f".{ item[:-3] }", package = __package__) 29 | if not hasattr(plugin, "export"): 30 | raise Exception(f"[Plugin Manager] Function 'export' must be stated in plugin file: { dir_path }/{ item }") 31 | plugin_export = getattr(plugin, "export")() 32 | plugin_list.append((module_name, plugin_export[0], plugin_export[1])) 33 | # import plugins in dirs 34 | if os.path.exists(f"{ dir_path }/{ item }/__init__.py") and item not in exception_dirs: 35 | plugin = importlib.import_module(f".{ item }", package = __package__) 36 | if not hasattr(plugin, "export"): 37 | raise Exception(f"[Plugin Manager] Function 'export' must be stated in plugin dir's '__init__.py' file: { dir_path }/{ item }/__init__.py") 38 | plugin_export = getattr(plugin, "export")() 39 | plugin_list.append((module_name, plugin_export[0], plugin_export[1])) 40 | 41 | return (plugin_list, default_settings) -------------------------------------------------------------------------------- /Agently/plugins/facility/config.ini: -------------------------------------------------------------------------------- 1 | [plugins] 2 | module_name = facility 3 | exception_files = __init__.py 4 | exception_dirs = utils -------------------------------------------------------------------------------- /Agently/plugins/facility/utils/FacilityABC.py: -------------------------------------------------------------------------------- 1 | #ABC = Abstract Base Class 2 | from abc import ABC 3 | 4 | class FacilityABC(ABC): 5 | def __init__(self, *, storage: object, plugin_manager: object, settings: object): 6 | self.storage = storage 7 | self.plugin_manager = plugin_manager 8 | self.settings = settings -------------------------------------------------------------------------------- /Agently/plugins/facility/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .FacilityABC import FacilityABC -------------------------------------------------------------------------------- /Agently/plugins/request/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | import configparser 4 | 5 | def export(): 6 | plugin_list = [] 7 | 8 | dir_path = os.path.dirname(os.path.abspath(__file__)) 9 | dir_name = os.path.basename(dir_path) 10 | 11 | # read config.ini 12 | config = configparser.ConfigParser() 13 | config.optionxform = str 14 | config.read(f"{ dir_path }/config.ini") 15 | config_is_not_empty = len(dict(config)) > 1 16 | 17 | # load configs 18 | module_name = config["plugins"]["module_name"] if config_is_not_empty and "module_name" in config else dir_name 19 | exception_files = set(config["plugins"]["exception_files"]) if config_is_not_empty and "exception_files" in config["plugins"] else set([]) 20 | exception_files.add("__init__.py") 21 | exception_dirs = set(config["plugins"]["exception_dirs"]) if config_is_not_empty and "exception_dirs" in config["plugins"] else set([]) 22 | exception_dirs.add("utils") 23 | default_settings = dict(config["default settings"]) if config_is_not_empty and "default settings" in config else None 24 | 25 | for item in os.listdir(dir_path): 26 | # import plugins in .py files 27 | if item.endswith('.py') and item not in exception_files: 28 | plugin = importlib.import_module(f".{ item[:-3] }", package = __package__) 29 | if not hasattr(plugin, "export"): 30 | raise Exception(f"[Plugin Manager] Function 'export' must be stated in plugin file: { dir_path }/{ item }") 31 | plugin_export = getattr(plugin, "export")() 32 | plugin_list.append((module_name, plugin_export[0], plugin_export[1])) 33 | # import plugins in dirs 34 | if os.path.exists(f"{ dir_path }/{ item }/__init__.py") and item not in exception_dirs: 35 | plugin = importlib.import_module(f".{ item }", package = __package__) 36 | if not hasattr(plugin, "export"): 37 | raise Exception(f"[Plugin Manager] Function 'export' must be stated in plugin dir's '__init__.py' file: { dir_path }/{ item }/__init__.py") 38 | plugin_export = getattr(plugin, "export")() 39 | plugin_list.append((module_name, plugin_export[0], plugin_export[1])) 40 | 41 | return (plugin_list, default_settings) -------------------------------------------------------------------------------- /Agently/plugins/request/config.ini: -------------------------------------------------------------------------------- 1 | [plugins] 2 | module_name = request 3 | exception_files = __init__.py, MiniMax.py 4 | exception_dirs = utils 5 | 6 | [default settings] 7 | current_model = OpenAI -------------------------------------------------------------------------------- /Agently/plugins/request/generate_ENIRE_access_token.sh: -------------------------------------------------------------------------------- 1 | echo "此脚本由Agent开发框架项目Agently.cn提供" 2 | echo "将帮助您通过输入百度云的API Key和Secret Key创建百度云Access Token,用于API调用" 3 | echo "更多相关信息可以阅读:https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Ilkkrb0i5" 4 | read -p "请输入您的API Key: " api_key 5 | read -p "请输入您的Secret Key: " secret_key 6 | access_token=$(curl "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${api_key}&client_secret=${secret_key}" | awk -F'"' '/access_token/{print $14}') 7 | echo "" 8 | echo "请复制下面的Access Token结果,Happy Coding!" 9 | echo $access_token -------------------------------------------------------------------------------- /Agently/plugins/request/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .RequestABC import RequestABC 2 | from .transform import to_prompt_structure, to_instruction, to_json_desc, find_json, replace_placeholder_keyword 3 | from .format import format_request_messages -------------------------------------------------------------------------------- /Agently/plugins/request/utils/format.py: -------------------------------------------------------------------------------- 1 | def format_request_messages(request_messages, model_settings): 2 | system_prompt = "" 3 | system_messages = [] 4 | chat_messages = [] 5 | role_list = ["user", "assistant"] 6 | current_role = 0 7 | for message in request_messages: 8 | if "role" not in message or "content" not in message: 9 | raise Exception(f"[Agently Request]: key 'role' and key 'content' must be contained in `request_messages`:\n{ str(request_messages) }") 10 | if message["role"] == "system": 11 | # no_multi_system_messages=True 12 | if model_settings.get_trace_back("message_rules.no_multi_system_messages"): 13 | if isinstance(message["content"], str): 14 | system_prompt += f"{ message['content'] }\n" 15 | elif isinstance(message["content"], list): 16 | for content in message["content"]: 17 | if "type" in content and content["type"] == "text": 18 | system_prompt += f"{ content['text'] }\n" 19 | else: 20 | system_prompt += f"{ str(content) }\n" 21 | else: 22 | system_prompt += f"{ str(message['content']) }\n" 23 | # no_multi_system_messages=False 24 | else: 25 | system_messages.append(message) 26 | else: 27 | # strict_orders=True 28 | if model_settings.get_trace_back("message_rules.strict_orders"): 29 | if len(chat_messages) == 0 and message["role"] != "user": 30 | chat_messages.append({ "role": "user", "content": [{"type": "text", "text": "What did we talked about?" }] }) 31 | current_role = not current_role 32 | if message["role"] == role_list[current_role]: 33 | chat_messages.append(message) 34 | current_role = not current_role 35 | else: 36 | content = f"{ chat_messages[-1]['content'] }\n{ message['content'] }" 37 | chat_messages[-1]['content'] = content 38 | # strict_orders=False 39 | else: 40 | chat_messages.append(message) 41 | # no_multi_system_messages=True 42 | if model_settings.get_trace_back("message_rules.no_multi_system_messages") and system_prompt != "": 43 | system_messages.append({ 44 | "role": "system", 45 | "content": [{ 46 | "type": "text", 47 | "text": system_prompt 48 | }] 49 | }) 50 | formatted_messages = system_messages.copy() 51 | formatted_messages.extend(chat_messages) 52 | # no_multi_type_messages=True 53 | if model_settings.get_trace_back("message_rules.no_multi_type_messages"): 54 | current_messages = formatted_messages.copy() 55 | formatted_messages = [] 56 | for message in current_messages: 57 | if isinstance(message["content"], str): 58 | formatted_messages.append(message) 59 | elif isinstance(message["content"], list): 60 | for content in message["content"]: 61 | if "type" in content and content["type"] == "text": 62 | formatted_messages.append({ "role": message["role"], "content": content["text"] }) 63 | else: 64 | formatted_messages.append({ "role": message["role"], "content": str(content) }) 65 | else: 66 | formatted_messages.append({ "role": message["role"], "content": str(content) }) 67 | return formatted_messages -------------------------------------------------------------------------------- /Agently/plugins/request/utils/transform.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import yaml 4 | 5 | def tuple_representer(dumper, data): 6 | return dumper.represent_list(data) 7 | 8 | yaml.add_representer(tuple, tuple_representer) 9 | 10 | def to_prompt_structure(prompt_dict: dict, layer_count: int=1, end: str=""): 11 | prompt = "" 12 | for key, content in prompt_dict.items(): 13 | prompt += f"{ '#' * layer_count } { key }:\n" 14 | if isinstance(content, dict): 15 | prompt += to_prompt_structure(content, layer_count + 1) + '\n' 16 | else: 17 | prompt += str(content) + '\n' 18 | if layer_count == 1: 19 | prompt += "\n" 20 | if layer_count == 1: 21 | prompt += end 22 | return prompt 23 | 24 | def to_instruction(origin): 25 | if origin == None: 26 | return None 27 | elif isinstance(origin, (list, tuple, set, dict)): 28 | return yaml.dump(origin, allow_unicode=True, sort_keys=False) 29 | else: 30 | return str(origin) 31 | 32 | def to_json_desc(origin, layer_count = 0): 33 | if isinstance(origin, dict): 34 | json_string = "" 35 | if layer_count > 0: 36 | json_string += "\n" 37 | json_string += ("\t" * layer_count) + "{\n" 38 | for key, value in origin.items(): 39 | json_string += ("\t" * (layer_count + 1)) + "\"" + key + "\": " + to_json_desc(value, layer_count + 1) + "\n" 40 | if layer_count > 0: 41 | json_string += ("\t" * (layer_count + 1)) + "}," 42 | else: 43 | json_string += "}" 44 | return json_string 45 | elif isinstance(origin, (list, set)): 46 | json_string = "" 47 | if layer_count > 0: 48 | json_string += "\n" 49 | json_string += ("\t" * layer_count) + "[\n" 50 | for item in origin: 51 | json_string += ("\t" * (layer_count + 1)) + to_json_desc(item, layer_count + 1) + ",\n" 52 | json_string += ("\t" * (layer_count + 1)) + "\\\\...\n" 53 | if layer_count > 0: 54 | json_string += ("\t" * layer_count) + "]," 55 | else: 56 | json_string += "]" 57 | return json_string 58 | elif isinstance(origin, tuple): 59 | if isinstance(origin[0], (dict, list, set)): 60 | json_string = f"\n{ to_json_desc(origin[0], layer_count + 1) }," 61 | else: 62 | json_string = f"<{ str(origin[0]) }>," 63 | if len(origin) >= 2: 64 | json_string += f"//{ str(origin[1]) }" 65 | return json_string 66 | else: 67 | return str(origin) 68 | 69 | def find_all_jsons(origin: str): 70 | pattern = r'"""(.*?)"""' 71 | origin = re.sub( 72 | pattern, 73 | lambda match: json.dumps(match.group(1)), 74 | origin, 75 | flags=re.DOTALL 76 | ) 77 | origin = origin.replace("\"\"\"", "\"").replace("[OUTPUT]", "$<>") 78 | stage = 1 79 | json_blocks = [] 80 | block_num = 0 81 | layer = 0 82 | skip_next = False 83 | in_quote = False 84 | for index, char in enumerate(origin): 85 | if skip_next: 86 | skip_next = False 87 | continue 88 | if stage == 1: 89 | if char == "\\": 90 | skip_next = True 91 | continue 92 | if char == "[" or char == "{": 93 | json_blocks.append(char) 94 | stage = 2 95 | layer += 1 96 | continue 97 | elif stage == 2: 98 | if not in_quote: 99 | if char == "\\": 100 | skip_next = True 101 | if origin[index + 1] == "\"": 102 | char = "\"" 103 | else: 104 | continue 105 | if char == "\"": 106 | in_quote = True 107 | if char == "[" or char == "{": 108 | layer += 1 109 | elif char == "]" or char == "}": 110 | layer -= 1 111 | elif char in ("\t", " ", "\n"): 112 | char = "" 113 | json_blocks[block_num] += char 114 | else: 115 | if char == "\\": 116 | char += origin[index + 1] 117 | skip_next = True 118 | elif char == "\n": 119 | char = "\\n" 120 | elif char == "\t": 121 | char = "\\t" 122 | elif char == "\"": 123 | in_quote = not in_quote 124 | json_blocks[block_num] += char 125 | if layer == 0: 126 | json_blocks[block_num] = json_blocks[block_num].replace("$<>", "[OUTPUT]") 127 | block_num += 1 128 | stage = 1 129 | return json_blocks 130 | 131 | def find_json(origin: str): 132 | result = find_all_jsons(origin) 133 | if len(result) > 0: 134 | return result[0] 135 | else: 136 | return None 137 | 138 | def replace_placeholder_keyword(keyword, target_keyword, source_text): 139 | if isinstance(source_text, str): 140 | pattern = re.compile(r'{' + re.escape(keyword) + r'(\.[^}]+)?}', re.IGNORECASE) 141 | return pattern.sub(lambda m: f'{{{ target_keyword }{m.group(1) or ""}}}', source_text) 142 | elif source_text: 143 | return source_text 144 | else: 145 | return None -------------------------------------------------------------------------------- /Agently/plugins/storage/FileStorage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from .utils import StorageABC 4 | 5 | class FileStorage(StorageABC): 6 | def __init__(self, db_name: str="default", settings: object={}): 7 | self.settings = settings 8 | self.path = self.settings.get("storage.FileStorage.path") or "./.Agently" 9 | self.db_name = db_name 10 | if not os.path.exists(self.path): 11 | os.mkdir(self.path) 12 | 13 | def __load_from_file(self, table_name: str): 14 | if not os.path.exists(f"{ self.path }/{ self.db_name }/{ table_name }.data"): 15 | return {} 16 | else: 17 | with open(f"{ self.path }/{ self.db_name }/{ table_name }.data", "r") as file: 18 | try: 19 | result = json.loads(file.read()) 20 | return result 21 | except Exception as e: 22 | print(f"[FileStorage] Error occurred when try to read file storage '{ self.db_name }/{ table_name }.data': { str(e) }") 23 | print(f"[FileStorage] '{ self.db_name }/{ table_name }.data' will be reset to {{}}.") 24 | self.__save_to_file(table_name, {}) 25 | return {} 26 | 27 | def __save_to_file(self, table_name: str, value: any): 28 | if not os.path.exists(f"{ self.path }/{ self.db_name }"): 29 | os.mkdir(f"{ self.path }/{ self.db_name }") 30 | if not isinstance(value, str): 31 | value = json.dumps(value) 32 | with open(f"{ self.path }/{ self.db_name }/{ table_name }.data", "w") as file: 33 | file.write(value) 34 | return True 35 | 36 | def set(self, table_name: str, key: str, value: any): 37 | table_data = self.__load_from_file(table_name) 38 | table_data.update({ key: json.dumps(value) }) 39 | self.__save_to_file(table_name, table_data) 40 | 41 | def set_all(self, table_name:str, full_data: dict): 42 | for key, value in full_data.items(): 43 | full_data[key] = json.dumps(value) 44 | self.__save_to_file(table_name, full_data) 45 | 46 | def remove(self, table_name: str, key: str): 47 | table_data = self.__load_from_file(table_name) 48 | if key in table_data: 49 | del table_data[key] 50 | self.__save_to_file(table_name, table_data) 51 | 52 | def update(self, table_name:str, update_data: dict): 53 | table_data = self.__load_from_file(table_name) 54 | for key, value in update_data.items(): 55 | table_data.update({ key: json.dumps(value) }) 56 | self.__save_to_file(table_name, table_data) 57 | 58 | def get(self, table_name: str, key: str): 59 | table_data = self.__load_from_file(table_name) 60 | if key in table_data: 61 | return json.loads(table_data[key]) 62 | else: 63 | return None 64 | 65 | def get_all(self, table_name: str, keys: (list, None)=None): 66 | table_data = self.__load_from_file(table_name) 67 | if keys: 68 | result = {} 69 | for key in keys: 70 | if key in table_data: 71 | result.update({ key: json.loads(table_data[key]) }) 72 | else: 73 | result.update({ key: None }) 74 | return result 75 | else: 76 | for key, value in table_data.items(): 77 | table_data[key] = json.loads(value) 78 | return table_data 79 | 80 | def export(): 81 | return ("FileStorage", FileStorage) -------------------------------------------------------------------------------- /Agently/plugins/storage/SQLite.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import sqlite3 4 | from .utils import StorageABC 5 | 6 | class SQLite(StorageABC): 7 | def __init__(self, db_name: str="default", settings: object={}): 8 | self.settings = settings 9 | self.path = self.settings.get("storage.SQLite.path") or None 10 | self.file_name = self.settings.get("storage.SQLite.file_name") or ".Agently.db" 11 | if self.path and not self.path.endswith("/"): 12 | self.path = self.path + "/" 13 | if not self.file_name.endswith(".db"): 14 | self.file_name = self.file_name + ".db" 15 | self.db = f"{ self.path }{ self.file_name }" if self.path else self.file_name 16 | self.space_name = db_name 17 | self.conn = None 18 | self.cursor = None 19 | 20 | def __connect(self): 21 | self.conn = sqlite3.connect(self.db) 22 | self.cursor = self.conn.cursor() 23 | 24 | def __connect_if_exists(self): 25 | if os.path.exists(self.db): 26 | self.conn = sqlite3.connect(self.db) 27 | self.cursor = self.conn.cursor() 28 | return True 29 | else: 30 | return False 31 | 32 | def __close(self): 33 | if self.conn: 34 | self.cursor.close() 35 | self.conn.close() 36 | self.conn = None 37 | self.cursor = None 38 | 39 | def __commit_and_close(self): 40 | if self.conn: 41 | self.conn.commit() 42 | self.cursor.close() 43 | self.conn.close() 44 | self.conn = None 45 | self.cursor = None 46 | 47 | def __create_table_if_not_exists(self, table_name: str): 48 | self.cursor.execute(f"CREATE TABLE IF NOT EXISTS `{ self.space_name }_{ table_name }` (key TEXT PRIMARY KEY, value TEXT)") 49 | 50 | def __drop_table_if_exists(self, table_name: str): 51 | self.cursor.execute(f"DROP TABLE IF EXISTS `{ self.space_name }_{ table_name }`") 52 | 53 | def set(self, table_name: str, key: str, value: any): 54 | self.__connect() 55 | self.__create_table_if_not_exists(table_name) 56 | self.cursor.execute( 57 | f"""INSERT INTO `{ self.space_name }_{ table_name }` (`key`, `value`) 58 | VALUES (?, ?) 59 | ON CONFLICT(`key`) 60 | DO UPDATE SET `key`=excluded.key, `value`=excluded.value 61 | """, 62 | (key, json.dumps(value)) 63 | ) 64 | self.__commit_and_close() 65 | return self 66 | 67 | def set_all(self, table_name:str, full_data: dict): 68 | self.__connect() 69 | self.__drop_table_if_exists(table_name) 70 | self.__create_table_if_not_exists(table_name) 71 | for key, value in full_data.items(): 72 | self.set(table_name, key, value) 73 | self.__commit_and_close() 74 | return self 75 | 76 | def remove(self, table_name: str, key: str): 77 | self.__connect() 78 | self.__create_table_if_not_exists(table_name) 79 | self.cursor.execute(f"DELETE FROM `{ self.space_name }_{ table_name }` WHERE `key` = ?", (key,)) 80 | self.__commit_and_close() 81 | return self 82 | 83 | def update(self, table_name:str, update_data: dict): 84 | self.__connect() 85 | self.__create_table_if_not_exists(table_name) 86 | for key, value in update_data.items(): 87 | self.set(table_name, key, value) 88 | self.__commit_and_close() 89 | return self 90 | 91 | def get(self, table_name: str, key: str): 92 | if self.__connect_if_exists(): 93 | try: 94 | self.cursor.execute(f"SELECT `value` FROM `{ self.space_name }_{ table_name }` WHERE `key` = ?", (key,)) 95 | result = self.cursor.fetchone() 96 | except sqlite3.OperationalError as e: 97 | result = None 98 | self.__close() 99 | if result: 100 | return json.loads(result[0]) 101 | else: 102 | return None 103 | else: 104 | return None 105 | 106 | def get_all(self, table_name: str, keys: (list, None)=None): 107 | if self.__connect_if_exists(): 108 | if keys: 109 | result = {} 110 | for key in keys: 111 | value = self.get(table_name, key) 112 | if value: 113 | result.update({ key: value }) 114 | else: 115 | result.update({ key: None }) 116 | self.__close() 117 | return result 118 | else: 119 | table_data = {} 120 | try: 121 | self.cursor.execute(f"SELECT `key`, `value` FROM `{ self.space_name }_{ table_name }`") 122 | results = self.cursor.fetchall() 123 | except sqlite3.OperationalError as e: 124 | results = [] 125 | for row in results: 126 | key, value = row[0], json.loads(row[1]) 127 | table_data.update({ key: value }) 128 | self.__close() 129 | return table_data 130 | else: 131 | return {} 132 | 133 | def export(): 134 | return ("SQLite", SQLite) -------------------------------------------------------------------------------- /Agently/plugins/storage/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | import configparser 4 | 5 | def export(): 6 | plugin_list = [] 7 | 8 | dir_path = os.path.dirname(os.path.abspath(__file__)) 9 | dir_name = os.path.basename(dir_path) 10 | 11 | # read config.ini 12 | config = configparser.ConfigParser() 13 | config.optionxform = str 14 | config.read(f"{ dir_path }/config.ini") 15 | config_is_not_empty = len(dict(config)) > 1 16 | 17 | # load configs 18 | module_name = config["plugins"]["module_name"] if config_is_not_empty and "module_name" in config else dir_name 19 | exception_files = set(config["plugins"]["exception_files"]) if config_is_not_empty and "exception_files" in config["plugins"] else set([]) 20 | exception_files.add("__init__.py") 21 | exception_dirs = set(config["plugins"]["exception_dirs"]) if config_is_not_empty and "exception_dirs" in config["plugins"] else set([]) 22 | exception_dirs.add("utils") 23 | default_settings = dict(config["default settings"]) if config_is_not_empty and "default settings" in config else None 24 | 25 | for item in os.listdir(dir_path): 26 | # import plugins in .py files 27 | if item.endswith('.py') and item not in exception_files: 28 | plugin = importlib.import_module(f".{ item[:-3] }", package = __package__) 29 | if not hasattr(plugin, "export"): 30 | raise Exception(f"[Plugin Manager] Function 'export' must be stated in plugin file: { dir_path }/{ item }") 31 | plugin_export = getattr(plugin, "export")() 32 | plugin_list.append((module_name, plugin_export[0], plugin_export[1])) 33 | # import plugins in dirs 34 | if os.path.exists(f"{ dir_path }/{ item }/__init__.py") and item not in exception_dirs: 35 | plugin = importlib.import_module(f".{ item }", package = __package__) 36 | if not hasattr(plugin, "export"): 37 | raise Exception(f"[Plugin Manager] Function 'export' must be stated in plugin dir's '__init__.py' file: { dir_path }/{ item }/__init__.py") 38 | plugin_export = getattr(plugin, "export")() 39 | plugin_list.append((module_name, plugin_export[0], plugin_export[1])) 40 | 41 | return (plugin_list, default_settings) -------------------------------------------------------------------------------- /Agently/plugins/storage/config.ini: -------------------------------------------------------------------------------- 1 | [plugins] 2 | module_name = storage 3 | exception_files = __init__.py 4 | exception_dirs = utils 5 | 6 | [default settings] 7 | storage_type = SQLite -------------------------------------------------------------------------------- /Agently/plugins/storage/utils/StorageABC.py: -------------------------------------------------------------------------------- 1 | #ABC = Abstract Base Class 2 | from abc import ABC, abstractmethod 3 | 4 | class StorageABC(ABC): 5 | @abstractmethod 6 | def __init__(self, db_name: str): 7 | self.db_name = db_name 8 | 9 | #Methods for key-value storage 10 | @abstractmethod 11 | def set(self, table_name: str, key: str, value: any): 12 | pass 13 | 14 | @abstractmethod 15 | def set_all(self, table_name:str, full_data: dict): 16 | pass 17 | 18 | @abstractmethod 19 | def remove(self, table_name: str, key: str): 20 | pass 21 | 22 | @abstractmethod 23 | def update(self, table_name:str, update_data: dict): 24 | pass 25 | 26 | @abstractmethod 27 | def get(self, table_name: str, key: str): 28 | pass 29 | 30 | @abstractmethod 31 | def get_all(self, table_name: str, keys: (list, None)=None): 32 | pass -------------------------------------------------------------------------------- /Agently/plugins/storage/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .StorageABC import StorageABC -------------------------------------------------------------------------------- /Agently/plugins/tool/Code.py: -------------------------------------------------------------------------------- 1 | from .utils import ToolABC 2 | 3 | class Code(ToolABC): 4 | def __init__(self, tool_manager: object): 5 | self.tool_manager = tool_manager 6 | 7 | def calculate(self, code: str, result_vars: list=None): 8 | results = { 9 | "origin": None, 10 | "for_agent": None, 11 | } 12 | global_vars = self.tool_manager.runtime_cache.get("global_vars", {}) 13 | local_vars = self.tool_manager.runtime_cache.get("local_vars", {}) 14 | exec(code, global_vars, local_vars) 15 | self.tool_manager.runtime_cache.update({ "global_vars": global_vars }) 16 | self.tool_manager.runtime_cache.update({ "local_vars": local_vars }) 17 | result = {} 18 | if result_vars: 19 | for result_var in result_vars: 20 | result.update({ result_var["why"]: local_vars.get(result_var["var_name"]) }) 21 | results = { 22 | "origin": result, 23 | "for_agent": result, 24 | } 25 | else: 26 | results = { 27 | "origin": local_vars, 28 | "for_agent": local_vars, 29 | } 30 | return results 31 | 32 | def export(self): 33 | return { 34 | "calculate": { 35 | "desc": "execute Python code to storage result to variables", 36 | "args": { 37 | "code": ("Python Code assigning values to variables", "[*Required]"), 38 | "result_vars": [{ 39 | "var_name": ("String", "[*Required]var name in {code}"), 40 | "why": ("String", "[*Required]brief purpose to get result of {var_name}") 41 | }], 42 | }, 43 | "func": self.calculate, 44 | }, 45 | } 46 | 47 | def export(): 48 | return ("Code", Code) -------------------------------------------------------------------------------- /Agently/plugins/tool/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import importlib 3 | import configparser 4 | 5 | def export(): 6 | plugin_list = [] 7 | 8 | dir_path = os.path.dirname(os.path.abspath(__file__)) 9 | dir_name = os.path.basename(dir_path) 10 | 11 | # read config.ini 12 | config = configparser.ConfigParser() 13 | config.optionxform = str 14 | config.read(f"{ dir_path }/config.ini") 15 | config_is_not_empty = len(dict(config)) > 1 16 | 17 | # load configs 18 | module_name = config["plugins"]["module_name"] if config_is_not_empty and "module_name" in config else dir_name 19 | exception_files = set(config["plugins"]["exception_files"]) if config_is_not_empty and "exception_files" in config["plugins"] else set([]) 20 | exception_files.add("__init__.py") 21 | exception_dirs = set(config["plugins"]["exception_dirs"]) if config_is_not_empty and "exception_dirs" in config["plugins"] else set([]) 22 | exception_dirs.add("utils") 23 | default_settings = dict(config["default settings"]) if config_is_not_empty and "default settings" in config else None 24 | 25 | for item in os.listdir(dir_path): 26 | # import plugins in .py files 27 | if item.endswith('.py') and item not in exception_files: 28 | plugin = importlib.import_module(f".{ item[:-3] }", package = __package__) 29 | if not hasattr(plugin, "export"): 30 | raise Exception(f"[Plugin Manager] Function 'export' must be stated in plugin file: { dir_path }/{ item }") 31 | plugin_export = getattr(plugin, "export")() 32 | plugin_list.append((module_name, plugin_export[0], plugin_export[1])) 33 | # import plugins in dirs 34 | if os.path.exists(f"{ dir_path }/{ item }/__init__.py") and item not in exception_dirs: 35 | plugin = importlib.import_module(f".{ item }", package = __package__) 36 | if not hasattr(plugin, "export"): 37 | raise Exception(f"[Plugin Manager] Function 'export' must be stated in plugin dir's '__init__.py' file: { dir_path }/{ item }/__init__.py") 38 | plugin_export = getattr(plugin, "export")() 39 | plugin_list.append((module_name, plugin_export[0], plugin_export[1])) 40 | 41 | return (plugin_list, default_settings) -------------------------------------------------------------------------------- /Agently/plugins/tool/config.ini: -------------------------------------------------------------------------------- 1 | [plugins] 2 | module_name = storage 3 | exception_files = __init__.py 4 | exception_dirs = utils 5 | 6 | [default settings] -------------------------------------------------------------------------------- /Agently/plugins/tool/utils/ToolABC.py: -------------------------------------------------------------------------------- 1 | #ABC = Abstract Base Class 2 | from abc import ABC, abstractmethod 3 | 4 | class ToolABC(ABC): 5 | def __init__(self, tool_manager: object): 6 | self.tool_manager = tool_manager 7 | 8 | def export(self): 9 | return { 10 | "": { 11 | "desc": "", 12 | "args": { 13 | "": ("", ""), 14 | }, 15 | "func": callable, 16 | }, 17 | } -------------------------------------------------------------------------------- /Agently/plugins/tool/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .ToolABC import ToolABC -------------------------------------------------------------------------------- /Agently/requirements.txt: -------------------------------------------------------------------------------- 1 | # Shell Interaction Optimization 2 | pyreadline3 3 | # Async Ensurence 4 | # nest_asyncio 5 | # Network 6 | httpx 7 | # Model Client 8 | openai 9 | litellm 10 | # Format 11 | PyYAML 12 | json5 13 | # Web 14 | duckduckgo_search 15 | beautifulsoup4 16 | requests 17 | # MCP 18 | mcp 19 | fastapi -------------------------------------------------------------------------------- /Agently/utils/AliasManager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import inspect 3 | import asyncio 4 | 5 | class AliasManager(object): 6 | def __init__(self, target: object): 7 | self.target = target 8 | self.alias_name_list = [] 9 | self.alias_details = {} 10 | 11 | def register(self, alias_name: str, alias_func: callable, *, return_value: bool=False, agent_component_name: str=None): 12 | if alias_name in self.alias_name_list: 13 | raise Exception(f"[Alias Manager] alias_name '{ alias_name }' has already been registered.") 14 | if return_value: 15 | setattr(self.target, alias_name, alias_func) 16 | else: 17 | if asyncio.iscoroutinefunction(alias_func): 18 | async def _alias_func(*args, **kwargs): 19 | await alias_func(*args, **kwargs) 20 | return self.target 21 | setattr(self.target, alias_name, _alias_func) 22 | else: 23 | def _alias_func(*args, **kwargs): 24 | alias_func(*args, **kwargs) 25 | return self.target 26 | setattr(self.target, alias_name, _alias_func) 27 | self.alias_name_list.append(alias_name) 28 | self.alias_details.update({ alias_name: { "func": alias_func, "return_value": return_value, "agent_component": agent_component_name } }) 29 | return self 30 | 31 | def empty_alias(self): 32 | self.alias_name_list = [] 33 | self.alias_details = {} 34 | return self 35 | 36 | def get_alias_info(self, *, group_by: str=None): 37 | result = {} 38 | if group_by == "agent_component": 39 | for alias_name, alias_info in self.alias_details.items(): 40 | if alias_info["agent_component"] == None: 41 | agent_component_name = "Unnamed" 42 | else: 43 | agent_component_name = alias_info["agent_component"] 44 | if agent_component_name not in result: 45 | result[agent_component_name] = {} 46 | result[agent_component_name][alias_name] = { "paramaters": [] } 47 | alias_func_info = inspect.signature(alias_info["func"]) 48 | for name, meta in alias_func_info.parameters.items(): 49 | result[agent_component_name][alias_name]["paramaters"].append(str(meta.name)) 50 | result[agent_component_name][alias_name]["is_return_value"] = alias_info["return_value"] 51 | result[agent_component_name][alias_name]["docstring"] = str(alias_info["func"].__doc__) 52 | else: 53 | for alias_name, alias_info in self.alias_details.items(): 54 | result[alias_name] = { "paramaters": [] } 55 | alias_func_info = inspect.signature(alias_info["func"]) 56 | for name, meta in alias_func_info.parameters.items(): 57 | result[alias_name]["paramaters"].append(str(meta.name)) 58 | result[alias_name]["is_return_value"] = alias_info["return_value"] 59 | result[alias_name]["docstring"] = str(alias_info["func"].__doc__) 60 | result[alias_name]["agent_component"] = alias_info["agent_component"] 61 | return result 62 | 63 | def print_alias_info(self, *, group_by: str=None): 64 | print(json.dumps(self.get_alias_info(group_by=group_by), indent=4, ensure_ascii=False)) -------------------------------------------------------------------------------- /Agently/utils/DataGenerator.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import queue 3 | 4 | class DataGeneratorTimeoutException(Exception): 5 | def __init__(self, message): 6 | self.message = message 7 | super().__init__(self.message) 8 | 9 | def __str__(self): 10 | return f"\n[Data Generator] Error: { str(self.message) }" 11 | 12 | class DataGeneratorEvent: 13 | def __init__(self, data_generator: "DataGenerator"): 14 | self.data_generator = data_generator 15 | self.handlers = {} 16 | 17 | def emit(self, event:str, data:any): 18 | self.data_generator.add({ 19 | "event": event, 20 | "data": data, 21 | }) 22 | return self 23 | 24 | def on(self, event:str, handler:callable): 25 | if event not in self.handlers: 26 | self.handlers.update({ event: [] }) 27 | self.handlers[event].append(handler) 28 | return self 29 | 30 | def clean(self, event:str): 31 | if event in self.handlers: 32 | del self.handlers[event] 33 | return self 34 | 35 | class DataGenerator: 36 | def __init__(self, *, timeout:int=None, allow_timeout:bool=False): 37 | self.thread = None 38 | self.queue = queue.Queue() 39 | self.timeout = timeout 40 | self.allow_timeout = allow_timeout 41 | self.event = DataGeneratorEvent(self) 42 | 43 | def future(self, todo_func: callable): 44 | self.thread = threading.Thread(target=todo_func, args=[self.start]) 45 | self.thread.start() 46 | return self 47 | 48 | def join(self): 49 | self.thread.join() 50 | return self 51 | 52 | def start(self): 53 | end_flag = False 54 | while True: 55 | try: 56 | if self.timeout: 57 | data = self.queue.get(timeout=self.timeout) 58 | else: 59 | data = self.queue.get() 60 | if data == "$END$": 61 | self.queue = queue.Queue() 62 | break 63 | else: 64 | if "event" in data: 65 | if data["event"] in self.event.handlers: 66 | for handler in self.event.handlers[data["event"]]: 67 | result = handler(data["data"]) 68 | if "yield" in result: 69 | yield result["yield"] 70 | if "end" in result and result["end"] == True: 71 | self.queue = queue.Queue() 72 | end_flag = True 73 | if end_flag: 74 | break 75 | else: 76 | yield data 77 | except queue.Empty: 78 | self.queue = queue.Queue() 79 | if self.allow_timeout: 80 | break 81 | else: 82 | raise DataGeneratorTimeoutException(f"Data generator timeout (wait { self.timeout } seconds). Use `.end()` to put an end to the data queue before `.start()`.") 83 | 84 | def add(self, data): 85 | self.queue.put(data) 86 | return self 87 | 88 | def end(self): 89 | self.queue.put("$END$") -------------------------------------------------------------------------------- /Agently/utils/IdGenerator.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | class IdGenerator(object): 4 | def __init__(self, namespace: str): 5 | self.namespace = namespace 6 | self.namespace_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, self.namespace) 7 | 8 | def create(self): 9 | individual_id = uuid.uuid4() 10 | return str(uuid.uuid5(self.namespace_uuid, str(individual_id))) -------------------------------------------------------------------------------- /Agently/utils/MCPClient.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Union 2 | from contextlib import AsyncExitStack 3 | from mcp import ClientSession, StdioServerParameters 4 | from mcp.client.stdio import stdio_client 5 | 6 | class MCPClient: 7 | def __init__(self, server_params: Union[Dict, StdioServerParameters]=None): 8 | self._server_params = ( 9 | server_params 10 | if isinstance(server_params, StdioServerParameters) or server_params is None 11 | else StdioServerParameters(**server_params) 12 | ) 13 | self._exit_stack = AsyncExitStack() 14 | self._read = None 15 | self._write = None 16 | self._session = None 17 | 18 | async def __aenter__(self): 19 | await self.connect() 20 | return self.session 21 | 22 | async def __aexit__(self, _, __, ___): 23 | await self.close() 24 | 25 | async def connect(self, server_params: StdioServerParameters=None): 26 | server_params = server_params or self._server_params 27 | if not server_params: 28 | raise RuntimeError("MCP server parameters are required.") 29 | self._read, self._write = await self._exit_stack.enter_async_context( 30 | stdio_client(server_params) 31 | ) 32 | self._session: ClientSession = await self._exit_stack.enter_async_context(ClientSession(self._read, self._write)) 33 | await self._session.initialize() 34 | 35 | async def close(self): 36 | await self._exit_stack.aclose() 37 | 38 | @property 39 | def session(self): 40 | if not self._session: 41 | raise RuntimeError("Use .connect() to connect a MCP server first.") 42 | return self._session 43 | 44 | def _transform_schema_to_type(self, schema): 45 | if isinstance(schema, dict): 46 | # Handle `anyOf` / `oneOf` 47 | if 'anyOf' in schema or 'oneOf' in schema: 48 | key = 'anyOf' if 'anyOf' in schema else 'oneOf' 49 | return " | ".join(self._transform_schema_to_type(sub) for sub in schema[key]) 50 | 51 | # Handle enums 52 | if 'enum' in schema: 53 | return " | ".join(f'"{v}"' for v in schema['enum']) 54 | 55 | # Handle const 56 | if 'const' in schema: 57 | val = schema['const'] 58 | return f"{repr(val)}" 59 | 60 | # Handle type 61 | t = schema.get('type') 62 | if t == 'string': 63 | return "string" 64 | elif t == 'number' or t == 'integer': 65 | return "number" 66 | elif t == 'boolean': 67 | return "boolean" 68 | elif t == 'null': 69 | return "null" 70 | elif t == 'array': 71 | items = schema.get('items', {'type': 'any'}) 72 | return f"Array<{self._transform_schema_to_type(items)}>" 73 | elif t == 'object': 74 | props = schema.get('properties', {}) 75 | required = schema.get('required', []) 76 | parts = [] 77 | for key, val in props.items(): 78 | optional = '?' if key not in required else '' 79 | parts.append(f"{key}{optional}: {self._transform_schema_to_type(val)}") 80 | return f"{{{', '.join(parts)}}}" 81 | elif t == 'any': 82 | return "any" 83 | 84 | return "any" 85 | 86 | async def get_tools_info(self): 87 | tools = await self.session.list_tools() 88 | tools_info = [] 89 | for tool in tools.tools: 90 | tools_info.append({ 91 | "tool_name": tool.name, 92 | "desc": tool.description, 93 | "kwargs": {} 94 | }) 95 | for key, value in tool.inputSchema["properties"].items(): 96 | type = self._transform_schema_to_type(value) 97 | desc = ( 98 | value["description"] 99 | if "description" in value 100 | else ( 101 | value["desc"] 102 | if "desc" in value 103 | else None 104 | ) 105 | ) 106 | tools_info[-1]["kwargs"].update({ 107 | key: (type, desc), 108 | }) 109 | return tools_info 110 | 111 | async def call_tool(self, tool_name: str, kwargs: dict): 112 | response = await self.session.call_tool(tool_name, kwargs) 113 | return response.content[0].text -------------------------------------------------------------------------------- /Agently/utils/PluginManager.py: -------------------------------------------------------------------------------- 1 | import json 2 | from .RuntimeCtx import RuntimeCtx 3 | from .transform import find_json 4 | 5 | class PluginManager(object): 6 | def __init__(self, *, parent: object=None): 7 | self.plugins_runtime_ctx = RuntimeCtx(parent = parent.plugins_runtime_ctx if parent else None) 8 | 9 | def register(self, module_name: str, plugin_name: str, plugin: callable): 10 | if module_name == "$": 11 | raise Exception("[Plugin Manager] Module name can not be '$'.") 12 | self.plugins_runtime_ctx.set(f"{ module_name }.{ plugin_name }", plugin) 13 | return self 14 | 15 | def set_settings(self, keys_with_dots: str, value: any): 16 | self.plugins_runtime_ctx.set(f"$.settings.{ keys_with_dots }", value) 17 | return self 18 | 19 | def update_settings(self, settings: dict): 20 | for key, value in settings.items(): 21 | if value.lower() == "true": 22 | settings[key] = True 23 | if value.lower() == "false": 24 | settings[key] = False 25 | json_string = find_json(value) 26 | if json_string != None and json_string != '': 27 | settings[key] = json.loads(json_string) 28 | self.set_settings(key, settings[key]) 29 | return self 30 | 31 | def get_settings(self, keys_with_dots: str=None, default: str=None): 32 | if keys_with_dots != None: 33 | return self.plugins_runtime_ctx.get_trace_back(f"$.settings.{ keys_with_dots }", default) 34 | else: 35 | return self.plugins_runtime_ctx.get_trace_back("$.settings", default) 36 | 37 | def get(self, module_name: str, plugin_name: str=None): 38 | plugins = self.plugins_runtime_ctx.get_trace_back() 39 | if plugins == None: 40 | plugins = {} 41 | if module_name not in plugins: 42 | raise Exception(f"[Plugin Manager] Module '{ module_name }' is not in plugins runtime_ctx.") 43 | if plugin_name != None: 44 | if plugin_name not in plugins[module_name]: 45 | raise Exception(f"[Plugin Manager] Plugin '{ plugin_name }' is not in the plugins runtime_ctx of module '{ module_name }'.") 46 | return plugins[module_name][plugin_name] 47 | else: 48 | return plugins[module_name] 49 | 50 | def get_agent_component_list(self): 51 | agent_components = self.get("agent_component") 52 | return agent_components.keys() -------------------------------------------------------------------------------- /Agently/utils/RuntimeCtx.py: -------------------------------------------------------------------------------- 1 | from .DataOps import DataOps, NamespaceOps 2 | 3 | class RuntimeCtxNamespace(NamespaceOps): 4 | def __init__(self, namespace_name: str, runtime_ctx: object, *, return_to: object=None): 5 | super().__init__(namespace_name, runtime_ctx, return_to = return_to) 6 | 7 | def get_trace_back(self, keys_with_dots: str=None, default: str=None): 8 | return self.data_ops.get_trace_back(f"{ self.namespace_name }.{ keys_with_dots }" if keys_with_dots else self.namespace_name, default) 9 | 10 | def get(self, keys_with_dots: str=None, default: str=None, *, trace_back=True): 11 | if trace_back: 12 | return self.get_trace_back(keys_with_dots, default) 13 | else: 14 | return self.data_ops.get(f"{ self.namespace_name }.{ keys_with_dots }" if keys_with_dots else self.namespace_name, default) 15 | 16 | class RuntimeCtx(DataOps): 17 | def __init__ (self, *, parent: object=None, no_copy: bool=False): 18 | self.parent = parent 19 | self.runtime_ctx_storage = {} 20 | super().__init__(target_data = self.runtime_ctx_storage, no_copy = no_copy) 21 | 22 | def __update_trace_back_result(self, parent_result, result): 23 | for key in result: 24 | if key not in parent_result: 25 | parent_result[key] = {} 26 | elif not isinstance(parent_result[key], dict): 27 | parent_result[key] = {} 28 | if isinstance(result[key], dict): 29 | self.__update_trace_back_result(parent_result[key], result[key]) 30 | else: 31 | parent_result[key] = result[key] 32 | return parent_result 33 | 34 | def get_trace_back(self, keys_with_dots:str=None, default: str=None): 35 | result = self.get(keys_with_dots, trace_back = False) 36 | parent_result = self.parent.get_trace_back(keys_with_dots) if self.parent else None 37 | if result or parent_result: 38 | if isinstance(result, dict): 39 | parent_result = parent_result if isinstance(parent_result, dict) else {} 40 | return self.__update_trace_back_result(parent_result, result) 41 | else: 42 | return result if result is not None else parent_result 43 | else: 44 | return default 45 | 46 | def get(self, keys_with_dots: str=None, default: str=None, *, trace_back=True): 47 | if trace_back: 48 | return self.get_trace_back(keys_with_dots, default) 49 | else: 50 | return super().get(keys_with_dots, default) -------------------------------------------------------------------------------- /Agently/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .RuntimeCtx import RuntimeCtx, RuntimeCtxNamespace 2 | from .StorageDelegate import StorageDelegate 3 | from .PluginManager import PluginManager 4 | from .AliasManager import AliasManager 5 | from .ToolManager import ToolManager 6 | from .IdGenerator import IdGenerator 7 | from .DataOps import DataOps, NamespaceOps 8 | from .transform import to_prompt_structure, to_json_desc, to_instruction, find_all_jsons, find_json 9 | from .check_version import check_version 10 | from .load_json import load_json, find_and_load_json 11 | from .DataGenerator import DataGenerator 12 | from .lexer.lexer import Lexer 13 | from .MCPClient import MCPClient -------------------------------------------------------------------------------- /Agently/utils/check_version.py: -------------------------------------------------------------------------------- 1 | import json 2 | import asyncio 3 | import threading 4 | import aiohttp 5 | import importlib.metadata 6 | 7 | async def check_version_async(): 8 | current_version = importlib.metadata.version("Agently") 9 | async with aiohttp.ClientSession() as session: 10 | async with session.get( 11 | f"http://api.agently.tech/meta/api/latest?version={ current_version }", 12 | headers = { "Content-Type": "application/json" }, 13 | ) as response: 14 | try: 15 | async for chunk in response.content.iter_chunks(): 16 | result = chunk[0].decode("utf-8") 17 | result = json.loads(result) 18 | stable_version = result["data"]["version"] if "version" in result["data"] else None 19 | latest_version = result["data"]["latest"] if "latest" in result["data"] else None 20 | tips_type = result["data"]["type"] 21 | tips_content = result["data"]["tips"] 22 | """ 23 | print("[Agently Version Check]") 24 | print("This check only works 1 time each day in debug model.") 25 | print(f"Current Installed Version: { current_version }") 26 | if stable_version: 27 | print(f"Stable Version: { stable_version }") 28 | if latest_version: 29 | print(f"Latest Version: { latest_version }") 30 | """ 31 | if tips_type != "" and tips_content != "": 32 | print(f"[{ tips_type.upper() }]{ tips_content }") 33 | except Exception as e: 34 | print(e) 35 | 36 | def run_check_version(): 37 | loop = asyncio.new_event_loop() 38 | asyncio.set_event_loop(loop) 39 | loop.run_until_complete(check_version_async()) 40 | loop.close() 41 | 42 | def check_version(global_storage, today): 43 | thread = threading.Thread(target = run_check_version) 44 | thread.start() 45 | #thread.join() 46 | global_storage.set("agently", "check_version_record", today) -------------------------------------------------------------------------------- /Agently/utils/lexer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | 4 | Copyright (c) 2024 Karminski 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Agently/utils/lexer/__init__.py: -------------------------------------------------------------------------------- 1 | from .lexer import Lexer 2 | 3 | __version__ = "0.0.4" 4 | 5 | __title__ = "streamingjson" 6 | __description__ = ( 7 | "A streamlined, user-friendly JSON streaming preprocessor, crafted in Python." 8 | ) 9 | __url__ = "https://github.com/karminski/streaming-json-py" 10 | __uri__ = __url__ 11 | __doc__ = f"{__description__} <{__uri__}>" 12 | 13 | __author__ = "Karminski" 14 | __email__ = "code.karminski@outlook.com" 15 | 16 | __license__ = "MIT" 17 | __copyright__ = "Copyright 2024 Karminski" 18 | 19 | __all__ = [ 20 | "Lexer", 21 | ] 22 | -------------------------------------------------------------------------------- /Agently/utils/lexer/lexer_helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | helper method for lexer 3 | """ 4 | 5 | 6 | def is_ignore_token(c): 7 | """ 8 | check if target character is ignore token 9 | """ 10 | return c in "\t\n\v\f\r " 11 | 12 | 13 | def match_stack(stack, tokens): 14 | """ 15 | check if target stack match given tokens 16 | """ 17 | pointer = len(stack) 18 | tokens_left = len(tokens) 19 | 20 | while True: 21 | tokens_left -= 1 22 | pointer -= 1 23 | if tokens_left < 0: 24 | break 25 | if pointer < 0: 26 | return False 27 | if stack[pointer] != tokens[tokens_left]: 28 | return False 29 | return True 30 | -------------------------------------------------------------------------------- /Agently/utils/lexer/lexer_tokens.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tokens for lexer 3 | """ 4 | 5 | # Token constants 6 | TOKEN_EOF = 0 # end-of-file 7 | TOKEN_IGNORED = 1 # \t', '\n', '\v', '\f', '\r', ' ' 8 | TOKEN_LEFT_BRACKET = 2 # [ 9 | TOKEN_RIGHT_BRACKET = 3 # ] 10 | TOKEN_LEFT_BRACE = 4 # { 11 | TOKEN_RIGHT_BRACE = 5 # } 12 | TOKEN_COLON = 6 # : 13 | TOKEN_DOT = 7 # . 14 | TOKEN_COMMA = 8 # , 15 | TOKEN_QUOTE = 9 # " 16 | TOKEN_ESCAPE_CHARACTER = 10 # \ 17 | TOKEN_SLASH = 11 # / 18 | TOKEN_NEGATIVE = 12 # - 19 | TOKEN_NULL = 13 # null 20 | TOKEN_TRUE = 14 # true 21 | TOKEN_FALSE = 15 # false 22 | TOKEN_ALPHABET_LOWERCASE_A = 16 # a 23 | TOKEN_ALPHABET_LOWERCASE_B = 17 # b 24 | TOKEN_ALPHABET_LOWERCASE_C = 18 # c 25 | TOKEN_ALPHABET_LOWERCASE_D = 19 # d 26 | TOKEN_ALPHABET_LOWERCASE_E = 20 # e 27 | TOKEN_ALPHABET_LOWERCASE_F = 21 # f 28 | TOKEN_ALPHABET_LOWERCASE_L = 22 # l 29 | TOKEN_ALPHABET_LOWERCASE_N = 23 # n 30 | TOKEN_ALPHABET_LOWERCASE_R = 24 # r 31 | TOKEN_ALPHABET_LOWERCASE_S = 25 # s 32 | TOKEN_ALPHABET_LOWERCASE_T = 26 # t 33 | TOKEN_ALPHABET_LOWERCASE_U = 27 # u 34 | TOKEN_ALPHABET_UPPERCASE_A = 28 # A 35 | TOKEN_ALPHABET_UPPERCASE_B = 29 # B 36 | TOKEN_ALPHABET_UPPERCASE_C = 30 # C 37 | TOKEN_ALPHABET_UPPERCASE_D = 31 # D 38 | TOKEN_ALPHABET_UPPERCASE_E = 32 # E 39 | TOKEN_ALPHABET_UPPERCASE_F = 33 # F 40 | TOKEN_NUMBER = 34 # number 41 | TOKEN_NUMBER_0 = 35 # 0 42 | TOKEN_NUMBER_1 = 36 # 1 43 | TOKEN_NUMBER_2 = 37 # 2 44 | TOKEN_NUMBER_3 = 38 # 3 45 | TOKEN_NUMBER_4 = 39 # 4 46 | TOKEN_NUMBER_5 = 40 # 5 47 | TOKEN_NUMBER_6 = 41 # 6 48 | TOKEN_NUMBER_7 = 42 # 7 49 | TOKEN_NUMBER_8 = 43 # 8 50 | TOKEN_NUMBER_9 = 44 # 9 51 | TOKEN_OTHERS = 45 # anything else in json 52 | 53 | # Token Symbols 54 | TOKEN_LEFT_BRACKET_SYMBOL = "[" 55 | TOKEN_RIGHT_BRACKET_SYMBOL = "]" 56 | TOKEN_LEFT_BRACE_SYMBOL = "{" 57 | TOKEN_RIGHT_BRACE_SYMBOL = "}" 58 | TOKEN_COLON_SYMBOL = ":" 59 | TOKEN_DOT_SYMBOL = "." 60 | TOKEN_COMMA_SYMBOL = "," 61 | TOKEN_QUOTE_SYMBOL = '"' 62 | TOKEN_ESCAPE_CHARACTER_SYMBOL = "\\" 63 | TOKEN_SLASH_SYMBOL = "/" 64 | TOKEN_NEGATIVE_SYMBOL = "-" 65 | TOKEN_ALPHABET_LOWERCASE_A_SYMBOL = "a" 66 | TOKEN_ALPHABET_LOWERCASE_B_SYMBOL = "b" 67 | TOKEN_ALPHABET_LOWERCASE_C_SYMBOL = "c" 68 | TOKEN_ALPHABET_LOWERCASE_D_SYMBOL = "d" 69 | TOKEN_ALPHABET_LOWERCASE_E_SYMBOL = "e" 70 | TOKEN_ALPHABET_LOWERCASE_F_SYMBOL = "f" 71 | TOKEN_ALPHABET_LOWERCASE_L_SYMBOL = "l" 72 | TOKEN_ALPHABET_LOWERCASE_N_SYMBOL = "n" 73 | TOKEN_ALPHABET_LOWERCASE_R_SYMBOL = "r" 74 | TOKEN_ALPHABET_LOWERCASE_S_SYMBOL = "s" 75 | TOKEN_ALPHABET_LOWERCASE_T_SYMBOL = "t" 76 | TOKEN_ALPHABET_LOWERCASE_U_SYMBOL = "u" 77 | TOKEN_ALPHABET_UPPERCASE_A_SYMBOL = "A" 78 | TOKEN_ALPHABET_UPPERCASE_B_SYMBOL = "B" 79 | TOKEN_ALPHABET_UPPERCASE_C_SYMBOL = "C" 80 | TOKEN_ALPHABET_UPPERCASE_D_SYMBOL = "D" 81 | TOKEN_ALPHABET_UPPERCASE_E_SYMBOL = "E" 82 | TOKEN_ALPHABET_UPPERCASE_F_SYMBOL = "F" 83 | TOKEN_NUMBER_0_SYMBOL = "0" 84 | TOKEN_NUMBER_1_SYMBOL = "1" 85 | TOKEN_NUMBER_2_SYMBOL = "2" 86 | TOKEN_NUMBER_3_SYMBOL = "3" 87 | TOKEN_NUMBER_4_SYMBOL = "4" 88 | TOKEN_NUMBER_5_SYMBOL = "5" 89 | TOKEN_NUMBER_6_SYMBOL = "6" 90 | TOKEN_NUMBER_7_SYMBOL = "7" 91 | TOKEN_NUMBER_8_SYMBOL = "8" 92 | TOKEN_NUMBER_9_SYMBOL = "9" 93 | 94 | 95 | # Token symbol map 96 | token_symbol_map = { 97 | TOKEN_EOF: "EOF", 98 | TOKEN_LEFT_BRACKET: "[", 99 | TOKEN_RIGHT_BRACKET: "]", 100 | TOKEN_LEFT_BRACE: "{", 101 | TOKEN_RIGHT_BRACE: "}", 102 | TOKEN_COLON: ":", 103 | TOKEN_DOT: ".", 104 | TOKEN_COMMA: ",", 105 | TOKEN_QUOTE: '"', 106 | TOKEN_ESCAPE_CHARACTER: "\\", 107 | TOKEN_SLASH: "/", 108 | TOKEN_NEGATIVE: "-", 109 | TOKEN_NULL: "null", 110 | TOKEN_TRUE: "true", 111 | TOKEN_FALSE: "false", 112 | TOKEN_ALPHABET_LOWERCASE_A: "a", 113 | TOKEN_ALPHABET_LOWERCASE_B: "b", 114 | TOKEN_ALPHABET_LOWERCASE_C: "c", 115 | TOKEN_ALPHABET_LOWERCASE_D: "d", 116 | TOKEN_ALPHABET_LOWERCASE_E: "e", 117 | TOKEN_ALPHABET_LOWERCASE_F: "f", 118 | TOKEN_ALPHABET_LOWERCASE_L: "l", 119 | TOKEN_ALPHABET_LOWERCASE_N: "n", 120 | TOKEN_ALPHABET_LOWERCASE_R: "r", 121 | TOKEN_ALPHABET_LOWERCASE_S: "s", 122 | TOKEN_ALPHABET_LOWERCASE_T: "t", 123 | TOKEN_ALPHABET_LOWERCASE_U: "u", 124 | TOKEN_ALPHABET_UPPERCASE_A: "A", 125 | TOKEN_ALPHABET_UPPERCASE_B: "B", 126 | TOKEN_ALPHABET_UPPERCASE_C: "C", 127 | TOKEN_ALPHABET_UPPERCASE_D: "D", 128 | TOKEN_ALPHABET_UPPERCASE_E: "E", 129 | TOKEN_ALPHABET_UPPERCASE_F: "F", 130 | TOKEN_NUMBER_0: "0", 131 | TOKEN_NUMBER_1: "1", 132 | TOKEN_NUMBER_2: "2", 133 | TOKEN_NUMBER_3: "3", 134 | TOKEN_NUMBER_4: "4", 135 | TOKEN_NUMBER_5: "5", 136 | TOKEN_NUMBER_6: "6", 137 | TOKEN_NUMBER_7: "7", 138 | TOKEN_NUMBER_8: "8", 139 | TOKEN_NUMBER_9: "9", 140 | } 141 | -------------------------------------------------------------------------------- /Agently/utils/lexer/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "streamingjson" 7 | version = "0.0.4" 8 | authors = [ 9 | { name="Karminski", email="code.karminski@outlook.com" }, 10 | ] 11 | description = "A streamlined, user-friendly JSON streaming preprocessor, crafted in Python." 12 | readme = "README.md" 13 | requires-python = ">=3.7" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | ] 19 | 20 | [project.urls] 21 | Homepage = "https://github.com/karminski/streaming-json-py" 22 | Issues = "https://github.com/karminski/streaming-json-py/issues" 23 | -------------------------------------------------------------------------------- /Agently/utils/load_json.py: -------------------------------------------------------------------------------- 1 | import json5 2 | from .transform import find_json 3 | from .lexer import Lexer 4 | 5 | def check_structure(origin: any, compare_target: any, position: str=""): 6 | errors = [] 7 | if isinstance(origin, dict): 8 | for key in origin.keys(): 9 | current_position = position + "." + key 10 | if key not in compare_target: 11 | errors.append(current_position[1:]) 12 | check_result = check_structure(origin[key], compare_target[key], current_position) 13 | if check_result != True: 14 | errors.extend(check_result) 15 | if isinstance(origin, list): 16 | for index, item in enumerate(origin): 17 | current_position = position + "." + str(index) 18 | if index in enumerate(compare_target): 19 | check_result = check_structure(item, compare_target[index], current_position) 20 | if check_result != True: 21 | errors.extend(check_result) 22 | if len(errors) == 0: 23 | return True 24 | else: 25 | return errors 26 | 27 | async def fix_json(origin: str, input_dict: any, output_dict: any, request: object, *, is_debug: bool=False, errors = list): 28 | try: 29 | fixed_result = await request\ 30 | .input({ 31 | "input": input_dict, 32 | "expect JSON String format": output_dict, 33 | "error JSON String": origin , 34 | "error info": errors, 35 | })\ 36 | .output('FIXED {error JSON String} JSON STRING ONLY WITHOUT EXPLANATION that can be parsed by Python')\ 37 | .start_async() 38 | json_string = find_json(fixed_result) 39 | if is_debug: 40 | print("[Cleaned JSON String]:\n", json_string) 41 | print("\n--------------------------\n") 42 | fixed_result = json5.loads(json_string) 43 | if is_debug: 44 | print("[Parse JSON to Dict] Done") 45 | print("\n--------------------------\n") 46 | return fixed_result 47 | except Exception as e: 48 | return f"$$$JSON_ERROR:[Agent Request] Error still occured when try to fix JSON decode error: { str(e) }\nOrigin JSON String:\n{ origin }" 49 | 50 | async def load_json(origin: str, input_dict: any, output_dict: any, request: object, *, is_debug: bool=False): 51 | try: 52 | json_string = find_json(origin) 53 | if is_debug: 54 | print("[Cleaned JSON String]:\n", json_string) 55 | print("\n--------------------------\n") 56 | parsed_dict = json5.loads(json_string) 57 | return parsed_dict 58 | except Exception as e: 59 | if is_debug: 60 | print("[JSON Decode Error Occurred] Start Fixing Process...") 61 | try: 62 | lexer = Lexer() 63 | lexer.append_string(find_json(origin)) 64 | return json5.loads(lexer.complete_json()) 65 | except Exception as e: 66 | return await fix_json(origin, input_dict, output_dict, request, is_debug = is_debug, errors = [str(e)]) 67 | 68 | def find_and_load_json(origin: str): 69 | try: 70 | json_string = find_json(origin) 71 | return json5.loads(json_string) 72 | except Exception as e: 73 | return 'Error' -------------------------------------------------------------------------------- /docs/guidebook/Agently_AI应用开发框架由浅入深的应用开发指导_应用开发者入门篇.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgentEra/Agently/528a8294f2531d3703efb5bcaa0fea4f9c5357a8/docs/guidebook/Agently_AI应用开发框架由浅入深的应用开发指导_应用开发者入门篇.pdf -------------------------------------------------------------------------------- /docs/guidebook/Agently_step_by_step_guide-simple.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgentEra/Agently/528a8294f2531d3703efb5bcaa0fea4f9c5357a8/docs/guidebook/Agently_step_by_step_guide-simple.pdf -------------------------------------------------------------------------------- /examples/deepseek_reasoning.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 5, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "[Reasoning]:\n", 13 | "Greetings! I'm DeepSeek-R1, an artificial intelligence assistant created by DeepSeek. I'm at your service and would be delighted to assist you with any inquiries or tasks you may have.\n", 14 | "\n", 15 | "[Reply]:\n", 16 | "Greetings! I'm DeepSeek-R1, an artificial intelligence assistant created by DeepSeek. I'm at your service and would be delighted to assist you with any inquiries or tasks you may have." 17 | ] 18 | } 19 | ], 20 | "source": [ 21 | "import Agently\n", 22 | "agent = (\n", 23 | " Agently.create_agent()\n", 24 | " .set_settings(\"current_model\", \"OAIClient\")\n", 25 | " .set_settings(\"model.OAIClient.url\", \"https://api.deepseek.com\")\n", 26 | " .set_settings(\"model.OAIClient.auth\", { \"api_key\": \"\" })\n", 27 | " .set_settings(\"model.OAIClient.options\", { \"model\": \"deepseek-reasoner\" })\n", 28 | ")\n", 29 | "\n", 30 | "response = agent.input(input(\"[Ask Something]:\")).get_generator()\n", 31 | "\n", 32 | "is_reasoning = False\n", 33 | "for event, data in response:\n", 34 | " if event == \"response:reasoning_delta\":\n", 35 | " if not is_reasoning:\n", 36 | " print(\"[Reasoning]:\")\n", 37 | " is_reasoning = True\n", 38 | " print(data, end=\"\", flush=True)\n", 39 | " if event == \"response:delta\":\n", 40 | " if is_reasoning:\n", 41 | " print(\"\\n\\n[Reply]:\")\n", 42 | " is_reasoning = False\n", 43 | " print(data, end=\"\", flush=True)" 44 | ] 45 | } 46 | ], 47 | "metadata": { 48 | "kernelspec": { 49 | "display_name": "3.9", 50 | "language": "python", 51 | "name": "python3" 52 | }, 53 | "language_info": { 54 | "codemirror_mode": { 55 | "name": "ipython", 56 | "version": 3 57 | }, 58 | "file_extension": ".py", 59 | "mimetype": "text/x-python", 60 | "name": "python", 61 | "nbconvert_exporter": "python", 62 | "pygments_lexer": "ipython3", 63 | "version": "3.9.19" 64 | } 65 | }, 66 | "nbformat": 4, 67 | "nbformat_minor": 2 68 | } 69 | -------------------------------------------------------------------------------- /examples/mcp/agent_as_mcp_server.py: -------------------------------------------------------------------------------- 1 | # 超快速把你的Agent创建为MCP Server 2 | import Agently 3 | 4 | fast_server = Agently.FastServer(type="mcp") 5 | 6 | agent = ( 7 | Agently.create_agent(is_debug=True) 8 | .set_settings("current_model", "OAIClient") 9 | .set_settings("model.OAIClient.url", "http://127.0.0.1:11434/v1") 10 | .set_settings("model.OAIClient.options.model", "qwen2.5-coder:14b") 11 | ) 12 | 13 | agent.set_agent_prompt("role", "你是编程专家") 14 | agent.set_agent_prompt("instruct", "你必须用中文输出") 15 | agent.use_mcp_server( 16 | command="python", 17 | args=["-u", "/Users/moxin/Library/Mobile Documents/com~apple~CloudDocs/projects/live/Agently_and_MCP/cocktail_server.py"], 18 | env=None, 19 | ) 20 | 21 | if __name__ == "__main__": 22 | fast_server.serve( 23 | agent, 24 | name="coder", 25 | desc="Ask any question about code or ask him to write a part of code for you." 26 | ) -------------------------------------------------------------------------------- /examples/mcp/mcp_server_example.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | from mcp.server.fastmcp import FastMCP 3 | 4 | mcp_server = FastMCP("Cocktail Info") 5 | 6 | @mcp_server.prompt() 7 | async def look_up_cocktail_info_prompt(message: str) -> str: 8 | return f"Please check cocktail information in '{ message }'. Cocktail name MUST BE translated into English!" 9 | 10 | @mcp_server.tool() 11 | async def look_up_cocktail_info(cocktail_name:str): 12 | """Look up cocktail information by name. 13 | The name MUST BE translated into English and the name MUST BE the basic name of cocktail. 14 | For Example: 15 | 'Margarita' is RIGHT! 16 | 'Spicy Mango Margarita' is WRONG!""" 17 | return ( 18 | httpx.get( 19 | "https://www.thecocktaildb.com/api/json/v1/1/search.php", 20 | params={ 21 | "s": cocktail_name, 22 | } 23 | ) 24 | .content.decode() 25 | ) 26 | 27 | if __name__ == "__main__": 28 | mcp_server.run() -------------------------------------------------------------------------------- /examples/mcp/mcp_tool_using.py: -------------------------------------------------------------------------------- 1 | import os 2 | import dotenv 3 | dotenv.load_dotenv(dotenv.find_dotenv()) 4 | 5 | import Agently 6 | 7 | agent = ( 8 | Agently.create_agent(is_debug=True) 9 | .set_settings("current_model", "OAIClient") 10 | # Use Ollama Local Model 11 | # .set_settings("model.OAIClient.url", "http://127.0.0.1:11434/v1") 12 | # .set_settings("model.OAIClient.options.model", "qwen2.5-coder:14b") 13 | # Or use DeepSeek API 14 | .set_settings("model.OAIClient.auth", { "api_key": os.environ.get("DEEPSEEK_API_KEY") }) 15 | .set_settings("model.OAIClient.options", { "model": "deepseek-chat" }) 16 | .set_settings("model.OAIClient.url", "https://api.deepseek.com/v1") 17 | ) 18 | 19 | # 超简单调用各种MCP Server(包括你自己创建的Agent/工作流 MCP Server) 20 | agent.use_mcp_server( 21 | command="python", 22 | args=["-u", ""], 23 | env=None, 24 | ).input("介绍一下长岛冰茶").start() 25 | 26 | # agent.use_mcp_server( 27 | # command="python", 28 | # args=["-u", ""], 29 | # env=None, 30 | # ).input("写一段求斐波那契数列第n位的代码").start() 31 | 32 | # agent.use_mcp_server( 33 | # command="python", 34 | # args=["-u", ""], 35 | # env=None, 36 | # ).input("今天旧金山天气怎么样").start() 37 | 38 | # agent.use_mcp_server( 39 | # config={ 40 | # "mcpServers": { 41 | # "weather_reporter": { 42 | # "command": "python", 43 | # "args": ["-u", ""] 44 | # }, 45 | # }, 46 | # } 47 | # ).input("今天旧金山天气怎么样").start() -------------------------------------------------------------------------------- /examples/mcp/workflow_as_mcp_server.py: -------------------------------------------------------------------------------- 1 | # 超快速把你制作的智能工作流创建为MCP Server 2 | import Agently 3 | 4 | workflow = Agently.Workflow() 5 | 6 | @workflow.chunk() 7 | def get_weather(inputs, storage): 8 | return { 9 | "temperature": 24, 10 | "general": "sunny", 11 | "windy": 2, 12 | "wet": 0.3, 13 | } 14 | 15 | ( 16 | workflow 17 | .connect_to("get_weather") 18 | .connect_to("END") 19 | ) 20 | 21 | fast_server = Agently.FastServer("mcp") 22 | 23 | if __name__ == "__main__": 24 | fast_server.serve( 25 | workflow, 26 | name="weather_reporter", 27 | desc="Get weather by submit city name to `message`" 28 | ) -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | ## **_Agently_ Playground** 2 | 3 | We'll present latest Agently framework powered application showcases here and you're more than welcome to share your showcases with us. [[Share You Case Here](https://github.com/AgentEra/Agently/issues/new)] 4 | 5 | ### Read Development Handbook 6 | 7 | [[Agently 3.0 Application Development Handbook](https://github.com/AgentEra/Agently/blob/main/docs/guidebook/application_development_handbook.ipynb)] 8 | 9 | ### Start Your Own Test Run 10 | 11 | You can quickly start your own test using these jupyter document template: 12 | 13 | [[Quick Start Template in English](https://github.com/AgentEra/Agently/blob/main/playground/test_run_template.ipynb)] | [[中文版快速试用模板](https://github.com/AgentEra/Agently/blob/main/playground/%E7%BA%BF%E4%B8%8A%E5%BF%AB%E9%80%9F%E8%AF%95%E7%94%A8%E6%A8%A1%E6%9D%BF.ipynb)] 14 | 15 | ### Latest Show Cases 16 | - **Agently Workflow Series**: 17 | - [01 - Let's Get Started by Building a Multi-Round Chat with Agently Workflow!](https://github.com/AgentEra/Agently/blob/main/playground/workflow_series_01_building_a_multi_round_chat.ipynb) 18 | - [02 - Using Condition to Branch Off](https://github.com/AgentEra/Agently/blob/main/playground/workflow_series_02_using_condition_to_branch_off.ipynb) 19 | - [03 - Using Decorator to Create Chunks Faster](https://github.com/AgentEra/Agently/blob/main/playground/workflow_series_03_using_decorator_to_create_chunks.ipynb) 20 | - [04 - Draw a Workflow Graph to Help You Observe the Workflow](https://github.com/AgentEra/Agently/blob/main/playground/workflow_series_04_draw_a_workflow_graph.ipynb) 21 | - [05 - Seperating Generation Steps and Reply to User in Every Step](https://github.com/AgentEra/Agently/blob/main/playground/workflow_series_05_seperating_generation_steps_and_reply_in_every_step.ipynb) 22 | - **[How to launch local open-source model to drive agent? ](https://github.com/AgentEra/Agently/blob/main/playground/using_local_open_source_model_to_drive_agents.ipynb)_Agently_ x Xinference**`🆕` 23 | - **"一句话就能..."Showcase合集** [[飞桨社区中文版](https://aistudio.baidu.com/projectdetail/7439200)] 24 | - **[How to create event listener functions with alias or decorator? `using ZhipuAI GLM-4!`](https://github.com/AgentEra/Agently/blob/main/playground/create_event_listeners_with_alias_or_decorator.ipynb)** 25 | - **[Summon a Genie 🧞‍♂️ (Function Decorator) to Generate Agent Powered Function in Runtime](https://github.com/AgentEra/Agently/blob/main/playground/generate_agent_powered_function_in_runtime_using_decorator.ipynb)** `💪 New Feature in v3.1.4` 26 | - **[How to let your agents use tools to enhance themselves?](https://github.com/AgentEra/Agently/blob/main/playground/using_tools_to_enhance_your_agent.ipynb)** `💪 New Feature in v3.1` 27 | - **[How to use AsyncIO and Agently to Manage Complex Process with Concurrency and Asynchronous Dependencies](https://github.com/AgentEra/Agently/blob/main/playground/concurrency_and_asynchornous_dependency.ipynb)** 28 | - **[Prediction according Given Data Set: GPT-3.5-turbo-1106 vs Gemini Pro](https://github.com/AgentEra/Agently/blob/main/playground/predict_data_according_given_data_set.ipynb)** 29 | - **[How to use GOOGLE GEMINI to generate line and choices for NPC in game](https://github.com/AgentEra/Agently/blob/main/playground/NPC_in_game_generate_choices_using_google_gemini.ipynb)** 30 | - **[How to route user to different agents according different Authorities](https://github.com/AgentEra/Agently/blob/main/playground/routing_to_different_agent_group_for_users_with_different_authorities.ipynb)** 31 | - **Agently快速上手案例集** [[飞桨社区合作中文版](https://aistudio.baidu.com/projectdetail/7178289)] `🔥 HOT` 32 | - **[How to find connectable pairs from data collection of text headers and tails](https://github.com/AgentEra/Agently/blob/main/playground/finding_connectable_pairs_from_text_tailers_and_headers.ipynb)** 33 | - **[How to create a LLM powered character that can change behaviors according mood status in chat](https://github.com/AgentEra/Agently/blob/main/playground/character_change_behaviours_according_mood_status.ipynb)** 34 | - **[How to create an agent help you to write ad copies according an image](https://github.com/AgentEra/Agently/blob/main/playground/writing_ad_copies_according_image.ipynb)** 35 | - **[How to create an agent to interview customer according survey form](https://github.com/AgentEra/Agently/blob/main/playground/survey_agent_asks_questions_according_form.ipynb)** 36 | - **[How to recheck, confirm and rewrite LLM agent's response before them sent to user?](https://github.com/AgentEra/Agently/blob/main/playground/human_step_in_before_reply.ipynb)** 37 | - **[How to convert long text to question-answer pairs?](https://github.com/AgentEra/Agently/blob/main/playground/long_text_to_qa_pairs.ipynb)** 38 | - **[How to create an agent help you transform natural language to SQL?](https://github.com/AgentEra/Agently/blob/main/playground/sql_generator.ipynb)** `🔥 HOT` 39 | 40 | --- 41 | 42 | **_Agently_ Framework** - Speed Up Your AI Agent Native Application Development 43 | -------------------------------------------------------------------------------- /playground/SQL_generator.py: -------------------------------------------------------------------------------- 1 | import Agently 2 | agent_factory = Agently.AgentFactory(is_debug = False) 3 | 4 | agent_factory\ 5 | .set_settings("current_model", "OpenAI")\ 6 | .set_settings("model.OpenAI.auth", { "api_key": "Your-API-Key-API-KEY" })\ 7 | .set_settings("model.OpenAI.url", "YOUR-BASE-URL-IF-NEEDED") 8 | 9 | agent = agent_factory.create_agent() 10 | 11 | meta_data = { 12 | "table_meta" : [ 13 | { 14 | "table_name": "user", 15 | "columns": [ 16 | { "column_name": "user_id", "desc": "identity of user", "value type": "Number" }, 17 | { "column_name": "gender", "desc": "gender of user", "value type": ["male", "female"] }, 18 | { "column_name": "age", "desc": "age of user", "value type": "Number" }, 19 | { "column_name": "customer_level", "desc": "level of customer account", "value type": [1,2,3,4,5] }, 20 | ] 21 | }, 22 | { 23 | "table_name": "order", 24 | "columns": [ 25 | { "column_name": "order_id", "desc": "identity of order", "value type": "Number" }, 26 | { "column_name": "customer_user_id", "desc": "identity of customer, same value as user_id", "value type": "Number" }, 27 | { "column_name": "item_name", "desc": "item name of this order", "value type": "String" }, 28 | { "column_name": "item_number", "desc": "how many items to buy in this order", "value type": "Number" }, 29 | { "column_name": "price", "desc": "how much of each item", "value type": "Number" }, 30 | { "column_name": "date", "desc": "what date did this order happend", "value type": "Date" }, 31 | ] 32 | }, 33 | ] 34 | } 35 | 36 | is_finish = False 37 | while not is_finish: 38 | question = input("请输入您的问题: ") 39 | show_thinking = None 40 | while str(show_thinking).lower() not in ("y", "n"): 41 | show_thinking = input("本次输出是否展现思考过程?[Y/N]: ") 42 | show_thinking = False if show_thinking.lower == "n" else True 43 | print("[正在生成...]") 44 | result = agent\ 45 | .input({ 46 | "table_meta": meta_data["table_meta"], 47 | "question": question 48 | })\ 49 | .instruct([ 50 | "output SQL to query the database according meta data:{table_meta} that can anwser the question:{question}", 51 | "output language: Chinese", 52 | ])\ 53 | .output({ 54 | "thinkings": ("String", "Your problem solving thinking step by step"), 55 | "SQL": ("String", "final SQL only"), 56 | })\ 57 | .start() 58 | if show_thinking: 59 | thinking_process = "\n".join(result["thinkings"]) 60 | print("[思考过程]\n", thinking_process) 61 | print("[SQL]\n", result["SQL"]) 62 | while str(is_finish).lower() not in ("y", "n"): 63 | is_finish = input("是否结束?[Y/N]: ") 64 | is_finish = False if is_finish.lower() == "n" else True 65 | -------------------------------------------------------------------------------- /playground/function_calling.py: -------------------------------------------------------------------------------- 1 | import Agently 2 | # turn on debug mode to watch processing logs 3 | agent_factory = Agently.AgentFactory(is_debug = True) 4 | 5 | # This time let's try ERNIE(文心大模型)-4 to drive function calling 6 | # You can switch to any model that Agently support 7 | agent_factory\ 8 | .set_settings("current_model", "ERNIE")\ 9 | .set_settings("model.ERNIE.model_name", "ERNIE")\ 10 | .set_settings("model.ERNIE.auth", { "aistudio": "" }) 11 | 12 | # First let's create a worker agent 13 | worker = agent_factory.create_agent() 14 | 15 | # Prepare tool descriptions (of course you can load them from your system storage) 16 | tools = { 17 | "weather_report": { 18 | "desc": "get weather report for the present time", 19 | "input_requirement": { 20 | "location": ("String", "your location") 21 | }, 22 | "func": lambda **kwargs: print("The weather is sunny right now.\n", kwargs) 23 | }, 24 | "weather_forecast": { 25 | "desc": "get weather forecast for the next 2-24 hours.", 26 | "input_requirement": { 27 | "location": ("String", "your location"), 28 | }, 29 | "func": lambda **kwargs: print("There'll be raining 3 hours later.\n", kwargs) 30 | }, 31 | "file_browser": { 32 | "desc": "Browse files that are given to.", 33 | "input_requirement": { 34 | "file_path": ("String", "File path that to be browsed."), 35 | "chunk_num": ("Number", "How many chunks to be output?"), 36 | "need_summarize": ("Boolean", "Do user need a summarize about the file?") 37 | }, 38 | "func": lambda **kwargs: print("File browse work done.\n", kwargs) 39 | }, 40 | } 41 | 42 | # Then let the worker agent to decide when and how to call the tools 43 | def call_tools(natural_language_input): 44 | #step 1. confirm tools to be used 45 | tools_desc = [] 46 | for tool_name, tool_info in tools.items(): 47 | tools_desc.append({ "name": tool_name, "desc": tool_info["desc"] }) 48 | tools_to_be_used = worker\ 49 | .input({ 50 | "input": natural_language_input, 51 | "tools": str(tools_desc) 52 | })\ 53 | .output([("String", "Tool name in {{input.tools}} to response {{input}}'s requirement.")])\ 54 | .start() 55 | #step 2. genrate parameters in one time 56 | tool_params = {} 57 | for tool_name in tools_to_be_used: 58 | tool_params[tool_name] = tools[tool_name]["input_requirement"] 59 | call_parameters = worker\ 60 | .input({ 61 | "input": natural_language_input, 62 | })\ 63 | .output(tool_params)\ 64 | .start() 65 | #step 3. call functions once a time and see the outcomes 66 | for tool_name, call_parameter in call_parameters.items(): 67 | tools[tool_name]["func"](**call_parameter) 68 | call_tools("Browse ./readme.pdf for me and chunk to 3 pieces without summarize and check Beijing's next 24 hours weather for me.") -------------------------------------------------------------------------------- /playground/mimir.py: -------------------------------------------------------------------------------- 1 | import Agently 2 | # Mimir is a figure in Norse mythology, renowned for his knowledge and wisdom 3 | # ask a question for different level of complexity, try it 4 | 5 | agent_factory = Agently.AgentFactory() 6 | 7 | level_dict = {"0": "Child", "1": "Teen", "2": "Adult", "3": "Expert"} 8 | 9 | user_chosen_label_index = None 10 | while user_chosen_label_index not in level_dict: 11 | user_chosen_label_index = input("[0]: Child\n[1]: Teen\n[2]: Adult\n[3]: Expert") 12 | 13 | chosen_label = level_dict[user_chosen_label_index] 14 | 15 | agent_factory.set_settings("current_model", "Google") \ 16 | .set_settings("model.Google.auth", {"api_key": "API_KEY"}) 17 | 18 | # main function 19 | agent = agent_factory.create_agent() \ 20 | .set_role("the student knowledge level", chosen_label) 21 | 22 | if chosen_label == "Child": 23 | agent.set_role("role", "you are an elementary teacher") \ 24 | .set_role("rules of actions", "First, it is necessary to determine the intention of the user input according to the {intention determination rules}, and then select the applicable according to the intention determination result to reply.") \ 25 | .set_role("intent determination rules", "answer the question with something that children can easily find in daily life, something in school, some scenario with their parents, something with going to the shop, a restaurant, common activities that children do or do with their parents ") \ 26 | .toggle_component("Search", True) 27 | 28 | elif chosen_label == "Teen": 29 | agent.set_role("role", "you are a Middle School teacher") \ 30 | .set_role("rules of actions", "First, it is necessary to determine the intention of the user input according to the {intention determination rules}, and then select the applicable according to the intention determination result to reply.") \ 31 | .set_role("intent determination rules", "answer the question with something that an average teen of 15 or 14 years could understand; at this age, teens have already completed elementary school knowledge") \ 32 | .toggle_component("Search", True) 33 | 34 | elif chosen_label == "Adult": 35 | agent.set_role("role", "you are an last year of High School teacher") \ 36 | .set_role("rules of actions", "First, it is necessary to determine the intention of the user input according to the {intention determination rules}, and then select the applicable according to the intention determination result to reply.") \ 37 | .set_role("intent determination rules", "answer the question with something that an adult, perhaps not an expert in the field, could understand; add more complexity than, for example, explaining something for a teenager or a child. Be careful not to add too much complexity to the answer.") \ 38 | .toggle_component("Search", True) 39 | 40 | elif chosen_label == "Expert": 41 | agent.set_role("role", "you are an Ph.D in the closest field") \ 42 | .set_role("rules of actions", "First, it is necessary to determine the intention of the user input according to the {intention determination rules}, and then select the applicable according to the intention determination result to reply.") \ 43 | .set_role("intent determination rules", "answer the question with an expert with five years of experience in the closest field possible. For example, if the question asks something related to electricity, the closest field is an Electric Engineer. Another example, if it's related to planes or airships, it's Aerospace engineering. The last one, if it's related to History, the closest field is a Ph.D. in History.") \ 44 | .toggle_component("Search", True) 45 | 46 | my_input = input("[Question]: ") 47 | reply = agent.input(my_input) \ 48 | .instruct(f"try to answer the question for the user as their knowledge level as {chosen_label}") \ 49 | .start() 50 | 51 | print(reply) 52 | -------------------------------------------------------------------------------- /playground/planner_workflow.py: -------------------------------------------------------------------------------- 1 | import Agently 2 | agent_factory = Agently.AgentFactory(is_debug=True) 3 | agent_factory\ 4 | .set_settings("current_model", "OpenAI")\ 5 | .set_settings("model.OpenAI.auth", {"api_key": "Your-API-Key-API-KEY"})\ 6 | .set_settings("model.OpenAI.url", "YOUR-BASE-URL-IF-NEEDED") 7 | 8 | # 第 1 步,创建 Workflow 实例 9 | workflow = Agently.Workflow(settings = { 10 | "max_execution_limit": 20 11 | }) 12 | 13 | # 第 3 步,流程编排 14 | # 15 | # 启动 --> 生成策划问题,判断信息完整度 -- [完整] --> 活动策划 --> 输出 16 | # ↑ | 17 | # 提问并收集信息 _[不完整]__| 18 | # 19 | 20 | # 3.1 创建 chunk 节点(chunk 的类型可以使用 workflow.executor.regist_executor 注册) 21 | # 入口节点 22 | start_chunk = workflow.schema.create_chunk( 23 | title = "活动方案策划", 24 | type = "Start" 25 | ) 26 | 27 | """生成策划问题,判断信息完整度""" 28 | question_design_agent = agent_factory.create_agent() 29 | questions_design_chunk = workflow.schema.create_chunk( 30 | title = "策划问题生成及判断节点", 31 | executor=lambda input_pkg, store: ( 32 | question_design_agent 33 | .input('\n'.join(store.get('已收集的信息', []))) 34 | .set_role('活动策划师') 35 | .instruct('根据用户问题及已收集的信息,判断要策划一个活动,是否还需要额外信息,如果需要则给出收集活动信息的提问') 36 | .output({'提问清单': [('String', '待收集信息的提问问题清单(一次不多于3个)')], '收集完成': ('Boolean', '判断信息是否已收集完成')}) 37 | .start() 38 | ), 39 | handles={ 40 | "inputs": [{"handle": "输入"}], 41 | "outputs": [{"handle": "提问清单"}, {"handle": "收集完成"}] 42 | } 43 | ) 44 | 45 | """收集用户问题""" 46 | collect_user_info_chunk = workflow.schema.create_chunk( 47 | title="提问并收集信息", 48 | executor=lambda input_pkg, store: store.set( 49 | '已收集的信息', 50 | store.get('已收集的信息', []) + 51 | [f'{question or "Please input:"}: {input(question or "Please input:")}' for question in input_pkg['问题清单']] 52 | ), 53 | handles={ 54 | "inputs": [{"handle": "问题清单"}, {"handle": "启动"}], 55 | "outputs": [{"handle": "用户回答"}] 56 | } 57 | ) 58 | 59 | """活动策划""" 60 | planner_agent = agent_factory.create_agent() 61 | planner_chunk = workflow.schema.create_chunk( 62 | title='活动策划', 63 | executor=lambda input_pkg, store: ( 64 | planner_agent 65 | .input('\n'.join(store.get('已收集的信息', []))) 66 | .set_role('活动策划师') 67 | .instruct('根据给到的信息,策划一个完整的活动方案') 68 | .output({'策划方案': ('String', '策划方案')}) 69 | .start() 70 | ), 71 | handles={ 72 | "inputs": [{"handle": "输入"}], 73 | "outputs": [{"handle": "策划方案"}] 74 | } 75 | ) 76 | 77 | """最终的输出打印""" 78 | output_chunk = workflow.schema.create_chunk( 79 | title='输出', 80 | executor = lambda input_pkg, store: print('Result: ', input_pkg) 81 | ) 82 | 83 | # 3.2 按要求连接各个 chunk 84 | start_chunk.connect_to(questions_design_chunk) # 从起点出发,连接到 ”策划问题生成节点“ 85 | 86 | # 将 ”策划问题生成节点“ 的提问清单结果连接到 ”提问并收集信息节点“ 87 | questions_design_chunk.handle('提问清单').connect_to( 88 | collect_user_info_chunk.handle('问题清单')) 89 | 90 | def judge_finished(is_finished): 91 | print('judge finished', is_finished) 92 | return is_finished == True 93 | # 如果信息已完整,则连接到 ”活动策划节点“ 开始策划,否则,找用户提问,直到信息收集完整 94 | ( 95 | questions_design_chunk.handle('收集完成') 96 | .if_condition(judge_finished) 97 | .connect_to(planner_chunk) 98 | .else_condition() 99 | .connect_to(collect_user_info_chunk.handle('启动')) 100 | ) 101 | 102 | # 用户回答完本轮问题后,将用户的回答信息交给 "策划问题生成节点" ,继续判断信息是否完整 103 | collect_user_info_chunk.connect_to(questions_design_chunk) 104 | 105 | # 最后将 ”活动策划" 结果返回 106 | planner_chunk.connect_to(output_chunk) 107 | 108 | # 第 4 步,执行 109 | workflow.start() -------------------------------------------------------------------------------- /playground/simple_role_play.py: -------------------------------------------------------------------------------- 1 | import Agently 2 | 3 | ## 启动Agent工厂 4 | agent_factory = Agently.AgentFactory() 5 | ## Agent工厂配置 6 | agent_factory\ 7 | .set_settings("model.OpenAI.auth", { "api_key": "Your-API-Key" })\ 8 | .set_settings("model.OpenAI.url", "YOUR-BASE-URL-IF-NEEDED")\ 9 | .set_settings("is_debug", True) 10 | ## 决定开关哪些组件 11 | agent_factory\ 12 | .toggle_component("Role", True) 13 | ## 所有Agent工厂生产的Agent会继承 14 | 15 | ## 输入输出 16 | def play_with_role_play_agent(role_play_desc, my_input): 17 | agent = agent_factory.create_agent() 18 | setting_result = agent\ 19 | .input(f"帮我设计一个符合{ role_play_desc }的设定的角色")\ 20 | .instruct("使用中文输出")\ 21 | .output({ 22 | "role": { 23 | "name": ("String", "该角色的名字"), 24 | "age": ("Number", "该角色的年龄"), 25 | "character": ("String", "该角色的性格特点和行为癖好"), 26 | "belief": ("String", "该角色的信仰"), 27 | }, 28 | "background_story": [("String", "该角色的背景故事,请用传记体分段描述")] 29 | })\ 30 | .start() 31 | print('角色设定完毕', setting_result) 32 | agent\ 33 | .set_role("name", setting_result["role"]["name"])\ 34 | .set_role("age", setting_result["role"]["age"])\ 35 | .set_role("character", setting_result["role"]["character"])\ 36 | .set_role("belief", setting_result["role"]["belief"])\ 37 | .extend_role("background_story", setting_result["background_story"]) 38 | print('角色装载完毕') 39 | reply = agent\ 40 | .input(my_input)\ 41 | .instruct("使用中文输出")\ 42 | .start() 43 | return reply 44 | print(play_with_role_play_agent("爱用emoji的猫娘", "你好,今天是个钓鱼的好天气,不是吗?")) -------------------------------------------------------------------------------- /playground/stream_output_in_streamlit.py: -------------------------------------------------------------------------------- 1 | """ 2 | Step 1: 3 | > pip install streamlit==1.33.0 Agently>=3.0.1 4 | Step 2: 5 | > streamlit run stream_output_in_streamlit.py 6 | """ 7 | 8 | import queue, threading, time 9 | import streamlit as st 10 | import Agently 11 | 12 | 13 | def agent_chat_generator(agent, message, session_id): 14 | """reply generator""" 15 | reply_queue = queue.Queue() 16 | agent.active_session(session_id) 17 | 18 | @agent.on_event("delta") 19 | def on_delta(data): 20 | reply_queue.put_nowait(data) 21 | 22 | @agent.on_event("done") 23 | def on_done(data): 24 | reply_queue.put_nowait("$STOP") 25 | 26 | agent_thread = threading.Thread(target=agent.input(message).start) 27 | agent_thread.start() 28 | while True: 29 | reply = reply_queue.get() 30 | if reply == "$STOP": 31 | break 32 | for r in list(reply): 33 | time.sleep(0.01) # for fluency output 34 | yield r 35 | agent_thread.join() 36 | agent.stop_session() 37 | 38 | 39 | st.title("ChatBot based on Agently") 40 | 41 | # init required params 42 | required_params = ["current_model", "base_url", "api_key", "session_id", "agent_id", "message_list"] 43 | for param in required_params: 44 | if param not in st.session_state: 45 | if param.endswith("_list"): 46 | st.session_state[param] = [] 47 | elif param.endswith("_dict"): 48 | st.session_state[param] = {} 49 | else: 50 | st.session_state[param] = "" 51 | 52 | # Display chat history 53 | for message in st.session_state.message_list: 54 | with st.chat_message(message["role"]): 55 | st.markdown(message["content"]) 56 | 57 | ################ 58 | ## Sidebar ## 59 | ################ 60 | 61 | with st.sidebar: 62 | st.title("Settings") 63 | st.session_state.current_model = st.selectbox("Current Model", ["Kimi", "OpenAI", "Claude3"]) 64 | st.session_state.base_url = st.text_input("Base URL", "") 65 | st.session_state.api_key = st.text_input("API Key", "", type="password") 66 | st.session_state.session_id = st.text_input("Session ID", "sess_1") 67 | st.session_state.agent_id = st.text_input("Agent ID", "demo_agent_1") 68 | 69 | ################ 70 | ## Main Page ## 71 | ################ 72 | 73 | if prompt := st.chat_input("ask something..."): 74 | if not st.session_state.api_key: 75 | st.warning('Please enter API key!', icon='⚠') 76 | st.session_state.message_list.append({"role": "user", "content": prompt}) 77 | with st.chat_message("user"): 78 | st.markdown(prompt) 79 | with st.chat_message("assistant"): 80 | current_model = st.session_state.current_model 81 | api_key = st.session_state.api_key 82 | base_url = st.session_state.base_url 83 | # Get agent 84 | agent_factory = ( 85 | Agently.AgentFactory() 86 | .set_settings("current_model", current_model) 87 | .set_settings(f"model.{current_model}.auth", {"api_key": api_key}) 88 | .set_settings(f"model.{current_model}.url", base_url) 89 | ) 90 | agent = agent_factory.create_agent(agent_id=st.session_state.agent_id) 91 | # Stream agent reply 92 | response = st.write_stream(agent_chat_generator(agent=agent, 93 | message=prompt, 94 | session_id=st.session_state.session_id)) 95 | st.session_state.message_list.append({"role": "assistant", "content": response}) 96 | -------------------------------------------------------------------------------- /playground/web_socket.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Server End 3 | ''' 4 | import Agently 5 | 6 | agent_factory = Agently.AgentFactory() 7 | 8 | agent_factory\ 9 | .set_settings("model_settings.auth", { "api_key": "Your-API-Key" })\ 10 | .set_settings("model_settings.url", "Your-Base-URL-if-needed") 11 | 12 | agent = agent_factory.create_agent('agent_server') 13 | agent\ 14 | .start_websocket_server() 15 | 16 | ''' 17 | Client End 18 | ''' 19 | import asyncio 20 | import Agently 21 | 22 | client = Agently.WebSocketClient().create_keep_alive(path="agent_server") 23 | result = client\ 24 | .send("alias", { "name": "input", "params": "Give me 7 words" })\ 25 | .send("alias", { "name": "output", "params": [ [("String",)] ] })\ 26 | .send("start")\ 27 | .on("delta", lambda data: print(data, end="")) 28 | 29 | print(result) 30 | 31 | # Yes, you can use the client request the agent server 32 | # almost like the way you use the agent instance interface locally 33 | -------------------------------------------------------------------------------- /playground/workflow.py: -------------------------------------------------------------------------------- 1 | import Agently 2 | agent_factory = Agently.AgentFactory(is_debug=True) 3 | agent_factory\ 4 | .set_settings("current_model", "OpenAI")\ 5 | .set_settings("model.OpenAI.auth", {"api_key": "Your-API-Key-API-KEY"})\ 6 | .set_settings("model.OpenAI.url", "YOUR-BASE-URL-IF-NEEDED") 7 | 8 | # 第 1 步,创建 Workflow 实例 9 | workflow = Agently.Workflow() 10 | 11 | # 第 2 步,实现 “判断用户意图是购物还是闲聊,路由到情感专家 or 销售导购” 的效果 12 | # 13 | # --> 销 售 14 | # 启动 --> 用户输入 --> 意图判断 --> 意图路由 --> 输出 15 | # --> 情感专家 16 | # 17 | 18 | # 2.1 创建 chunk 节点(chunk 的类型可以使用 workflow.executor.regist_executor 注册) 19 | 20 | # 入口节点 21 | start_chunk = workflow.schema.create_chunk( 22 | title = '一个例子', 23 | type = 'Start' 24 | ) 25 | 26 | # 用户输入 27 | def input_executor(input_pkg, store): 28 | user_input = input('有什么我能帮你的吗?') 29 | store.set('用户问题', user_input) # 暂存数据 30 | return user_input 31 | 32 | wait_user_input_chunk = workflow.schema.create_chunk( 33 | title = '接收用户输入', 34 | executor = input_executor 35 | ) 36 | 37 | # 意图判断节点 38 | judge_agent = agent_factory.create_agent() 39 | judge_chunk = workflow.schema.create_chunk( 40 | title='意图判断', 41 | executor=lambda input_pkg, store: judge_agent.input(input_pkg['用户问题']).set_role('导购').instruct('判断用户意图是“闲聊”还是“购物”').output({ 42 | '用户意图': ('String', '\"闲聊\" 还是 \"购物\"') 43 | }).start(), 44 | handles = { 45 | "inputs": [{"handle": "用户问题"}], 46 | "outputs": [{"handle": "用户意图"}] 47 | } 48 | ) 49 | 50 | # 销售 51 | sales_agent = agent_factory.create_agent() 52 | sales_agent_chunk = workflow.schema.create_chunk( 53 | title='销售', 54 | executor=lambda input_pkg, store: sales_agent.input(store.get('用户问题')).set_role('百货超市的销售').instruct('向用户推销产品,引导用户购买').output({ 55 | '回复': ('String', '回答用户的问题') 56 | }).start(), 57 | handles = { 58 | "inputs": [{"handle": "用户问题"}], 59 | "outputs": [{"handle": "回复"}] 60 | } 61 | ) 62 | 63 | # 情感专家 64 | chat_agent = agent_factory.create_agent() 65 | chat_agent_chunk = workflow.schema.create_chunk( 66 | title = '情感专家', 67 | executor=lambda input_pkg, store: chat_agent.input(store.get('用户问题')).set_role('情感专家').output({ 68 | '回复': ('String', '回答用户的问题') 69 | }).start(), 70 | handles = { 71 | "inputs": [{"handle": "用户问题"}], 72 | "outputs": [{"handle": "回复"}] 73 | } 74 | ) 75 | 76 | # 最终的输出打印 77 | output_chunk = workflow.schema.create_chunk( 78 | executor=lambda input_pkg, store: print('[Result]', input_pkg) 79 | ) 80 | 81 | # 3.2 按要求连接各个 chunk 82 | start_chunk.connect_to(wait_user_input_chunk) # 从起点出发,连接到 ”用户输入节点“ 83 | 84 | wait_user_input_chunk.connect_to(judge_chunk) # 将 ”用户输入“ 的内容连接到 ”意图识别“ 85 | 86 | # 将 ”意图识别“ 的 "用户意图" 结果,连接到 ”判断路由“ 做判断 87 | ( 88 | judge_chunk.handle('用户意图') 89 | .if_condition(lambda target: target == '购物').connect_to(sales_agent_chunk) # 如果是购物,则路由到 ”销售“ 90 | .else_condition().connect_to(chat_agent_chunk) # 否则,路由到 ”情感专家“ 91 | ) 92 | 93 | sales_agent_chunk.connect_to(output_chunk) # 最后将 ”销售“ 的回复返回 94 | chat_agent_chunk.connect_to(output_chunk) # 最后将 ”情感专家" 的回复返回 95 | 96 | # 第 4 步,执行 97 | workflow.start() 98 | -------------------------------------------------------------------------------- /playground/workflow_gernerate_code.py: -------------------------------------------------------------------------------- 1 | import Agently 2 | import subprocess 3 | 4 | """ 5 | 根据用户输入的要求生成代码,在本地通过subprocess调用python执行代码执行生成代码,如运行发生错误,反馈错误,用户补充信息和前一轮生成的代码给llm,重新生成代码。 6 | """ 7 | agent = ( 8 | Agently.create_agent() 9 | .set_settings("current_model", "OpenAI") \ 10 | .set_settings("model.OpenAI.auth", {"api_key": "填入你的api-key"}) \ 11 | .set_settings("model.OpenAI.options", {"model": "gpt-4o-all"}) \ 12 | ) 13 | workflow = Agently.Workflow() 14 | 15 | 16 | @workflow.chunk() 17 | def generate_code(inputs, storage): 18 | if storage.get("question") is not None: 19 | question = storage.get("question") 20 | else: 21 | question = input("[User]: ") 22 | storage.set("question", question) 23 | additional_context = storage.get("additional_context", "") 24 | 25 | history_code = storage.get("history_code", "") 26 | if history_code != "": 27 | history_code = history_code["code"] 28 | prompt = f"{question}\n此前代码:\n{history_code}\n{additional_context}" 29 | print(prompt) 30 | code = ( 31 | agent 32 | .general("输出规定", "输出可以运行的python代码,不要包含任何解释说明,不要包含markdown语法") 33 | .info("python代码要求", "必须有print语句显示运行结果") 34 | .set_role("工作规则," 35 | "1:如果用户输入中包含第三方package,必须先搜索package的使用说明,使用正确的,未过时的函数名称和参数名称。" 36 | "2:如果返回的additional_context中包含错误信息,根据错误信息,修改代码。") 37 | .input(prompt) 38 | .output({"code": ("str", "return python code")}) 39 | .start() 40 | ) 41 | storage.set("generated_code", code) 42 | storage.set("history_code", code) 43 | print("[Generated Code]:", code) 44 | return code 45 | 46 | 47 | @workflow.chunk() 48 | def execute_code(inputs, storage): 49 | code = storage.get("generated_code")["code"] 50 | print(type(code)) 51 | print(code) 52 | try: 53 | result = subprocess.check_output(["python", "-c", code], stderr=subprocess.STDOUT, text=True, encoding='utf-8') 54 | print(result) 55 | storage.set("execution_result", result) 56 | return {"success": True} 57 | except subprocess.CalledProcessError as e: 58 | storage.set("execution_error", e.output) 59 | print("[Execution Error]:", e.output) 60 | return {"success": False} 61 | 62 | 63 | @workflow.chunk() 64 | def check_execution(inputs, storage): 65 | print("inputs:", inputs) 66 | if inputs["default"]["success"]: 67 | result = storage.get("execution_result") 68 | print("[Execution Result]:", result) 69 | else: 70 | error = storage.get("execution_error") 71 | print("[Execution Error]:", error) 72 | 73 | user_feedback = input("Is the program result correct? (Y/N): ") 74 | if user_feedback.upper() == "Y": 75 | return "success" 76 | else: 77 | if not inputs["default"]["success"]: 78 | storage.set("additional_context", storage.get("execution_error", "") + user_feedback.upper()) 79 | else: 80 | storage.set("additional_context", user_feedback.upper()) 81 | return "error" 82 | 83 | 84 | @workflow.chunk() 85 | def goodbye(inputs, storage): 86 | # 保存代码,把代码写入文件 87 | with open("generated_code.py", "w") as f: 88 | f.write(storage.get("generated_code")["code"]) 89 | print("Bye~👋") 90 | return 91 | 92 | 93 | workflow.connect_to("generate_code") 94 | ( 95 | workflow.chunks["generate_code"] 96 | .connect_to("execute_code") 97 | ) 98 | workflow.chunks["execute_code"].connect_to("check_execution") 99 | 100 | workflow.chunks["check_execution"].if_condition(lambda return_value, storage: return_value == "success").connect_to( 101 | "goodbye").connect_to("end").else_condition().connect_to("generate_code") 102 | 103 | workflow.start() 104 | 105 | # eg 读取本地图片file_path="face.png",使用红色方框标记所有人脸,并在图片左上角打印每个方框中心点的坐标[(x,y),(x,y)] 106 | # User intervention 坐标点不要打印在方框的上方,而是用列表的方式打印在图片的左上角,方框的中心点用绿色标记出来。 107 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "agently" 3 | version = "3.5.1.0" 4 | description = "[GenAI Application Development Framework] - 🚀 Build GenAI application quick and easy 💬 Easy to interact with GenAI agent in code using structure data and chained-calls syntax 🧩 Use Agently Workflow to manage complex GenAI working logic 🔀 Switch to any model without rewrite application code" 5 | authors = [ 6 | {name = "Agently Team",email = "developer@agently.tech"} 7 | ] 8 | license = {text = "Apache-2.0"} 9 | readme = "README.md" 10 | requires-python = ">=3.10" 11 | dependencies = [ 12 | "httpx (>=0.28.1,<0.29.0)", 13 | "pyreadline3 (>=3.5.4,<4.0.0)", 14 | "openai (>=1.61.1,<2.0.0)", 15 | "litellm (>=1.61.0,<2.0.0)", 16 | "pyyaml (>=6.0.2,<7.0.0)", 17 | "json5 (>=0.10.0,<0.11.0)", 18 | "duckduckgo-search (>=7.3.2,<8.0.0)", 19 | "beautifulsoup4 (>=4.13.3,<5.0.0)", 20 | "requests (>=2.32.3,<3.0.0)", 21 | "mcp (>=1.6.0,<2.0.0)", 22 | "fastapi (>=0.115.12,<0.116.0)", 23 | "boto3 (>=1.33.0,<2.0.0)" 24 | ] 25 | 26 | 27 | [build-system] 28 | requires = ["poetry-core>=2.0.0,<3.0.0"] 29 | build-backend = "poetry.core.masonry.api" 30 | --------------------------------------------------------------------------------