diff --git a/agentuniverse/agent/action/knowledge/knowledge.py b/agentuniverse/agent/action/knowledge/knowledge.py index b1787d19..3a4d6cde 100644 --- a/agentuniverse/agent/action/knowledge/knowledge.py +++ b/agentuniverse/agent/action/knowledge/knowledge.py @@ -9,7 +9,7 @@ import re import traceback from typing import Optional, Dict, List, Any -from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED +from concurrent.futures import wait, ALL_COMPLETED from langchain_core.utils.json import parse_json_markdown from langchain.tools import Tool as LangchainTool @@ -29,6 +29,7 @@ from agentuniverse.base.component.component_base import ComponentBase from agentuniverse.base.component.component_enum import ComponentEnum from agentuniverse.base.util.logging.logging_util import LOGGER +from agentuniverse.agent_serve.web.thread_with_result import ThreadPoolExecutorWithReturnValue class Knowledge(ComponentBase): @@ -75,17 +76,17 @@ class Config: rag_router: str = "base_router" post_processors: List[str] = [] readers: Dict[str, str] = dict() - insert_executor: Optional[ThreadPoolExecutor] = None - query_executor: Optional[ThreadPoolExecutor] = None + insert_executor: Optional[ThreadPoolExecutorWithReturnValue] = None + query_executor: Optional[ThreadPoolExecutorWithReturnValue] = None ext_info: Optional[Dict] = None def __init__(self, **kwargs): super().__init__(component_type=ComponentEnum.KNOWLEDGE, **kwargs) - self.insert_executor = ThreadPoolExecutor( + self.insert_executor = ThreadPoolExecutorWithReturnValue( max_workers=5, thread_name_prefix="Knowledge store" ) - self.query_executor = ThreadPoolExecutor( + self.query_executor = ThreadPoolExecutorWithReturnValue( max_workers=10, thread_name_prefix="Knowledge query" ) diff --git a/agentuniverse/agent/action/knowledge/store/graph_document.py b/agentuniverse/agent/action/knowledge/store/graph_document.py new file mode 100644 index 00000000..2b12f04c --- /dev/null +++ b/agentuniverse/agent/action/knowledge/store/graph_document.py @@ -0,0 +1,25 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/7/22 18:16 +# @Author : fanen.lhy +# @Email : fanen.lhy@antgroup.com +# @FileName: image_document.py +from .document import Document + +try: + import pandas as pd +except ImportError: + raise ImportError( + "The functionality you are trying to use requires the pandas and pandas package. " + "You can install it by running: pip install pandas" + ) + + +class GraphDocument(Document): + """The basic class for an ImageDocument. + + Attributes: + graph_data: A pandas dataframe contents all results of neo4j cypher + """ + graph_data: pd.DataFrame diff --git a/agentuniverse/agent/action/knowledge/store/neo4j_store.py b/agentuniverse/agent/action/knowledge/store/neo4j_store.py new file mode 100644 index 00000000..a24f6f9c --- /dev/null +++ b/agentuniverse/agent/action/knowledge/store/neo4j_store.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/3/22 16:31 +# @Author : wangchongshi +# @Email : wangchongshi.wcs@antgroup.com +# @FileName: neo4j_store.py + +import json +from typing import List, Any, Optional +try: + from neo4j import GraphDatabase, AsyncGraphDatabase + import pandas as pd +except ImportError: + raise ImportError( + "The functionality you are trying to use requires the neo4j and pandas package. " + "You can install it by running: pip install neo4j pandas" + ) + + +from agentuniverse.agent.action.knowledge.store.graph_document import GraphDocument +from agentuniverse.agent.action.knowledge.store.document import Document +from agentuniverse.agent.action.knowledge.store.query import Query +from agentuniverse.agent.action.knowledge.store.store import Store +from agentuniverse.base.config.component_configer.component_configer import \ + ComponentConfiger + + +class Neo4jStore(Store): + + uri: Optional[str] = None + user: Optional[str] = None + password: Optional[str] = None + database: Optional[str] = None + driver: Any = None + async_driver: Any = None + + def _new_client(self) -> Any: + if self.database: + self.driver = GraphDatabase.driver(self.uri, auth=(self.user, self.password), database=self.database) + else: + self.driver = GraphDatabase.driver(self.uri, + auth=(self.user, self.password)) + + def _new_async_client(self) -> Any: + if self.database: + self.async_driver = AsyncGraphDatabase.driver(self.uri, + auth=(self.user, self.password), + database=self.database) + else: + self.async_driver = AsyncGraphDatabase.driver(self.uri, + auth=(self.user, self.password)) + + def execute_cypher(self, query_str, param=None, return_data=True): + df_result = self._execute_cypher(self.driver.session(), query_str, param, return_data) + return df_result + + @staticmethod + def _execute_cypher(session, query_str, param=None, return_data=True): + df_result = pd.DataFrame() + if param is None: + result = session.run(query_str) + else: + result = session.run(query_str, **param) + + if return_data: + result_list = [] + for resulti in result: + result_list.append(dict(resulti)) + + df_result = pd.DataFrame(result_list) + session.close() + return df_result + + + def query(self, query: Query, **kwargs) -> List[Document]: + query_type = query.ext_info.get("query_type", "") + + if query_type == "direct_cypher": + cypher_query = query.query_str + query_params = query.ext_info.get("query_params", {}) + records = self.execute_cypher(cypher_query, query_params) + return self._records_to_documents(cypher_query, records) + + elif query_type == "llm_generate_cypher": + schema_info = self._get_graph_schema_info() + + llm_cypher = self._llm_generate_cypher( + query.query_str, + schema_info + ) + query_params = query.ext_info.get("query_params", {}) + records = self.execute_cypher(llm_cypher, query_params) + return self._records_to_documents(query.query_str, records) + + elif query_type == "node_ids_query": + node_ids = query.query_str + if not node_ids: + return [] + cypher_query = self._build_node_ids_query(json.loads(query.query_str)) + query_params = query.ext_info.get("query_params", {}) + records = self.execute_cypher(cypher_query, query_params) + return self._records_to_documents(node_ids, records) + else: + raise NotImplementedError('This query type is not allowed in neo4j store.') + + def _records_to_documents(self, text, records: pd.DataFrame) -> List[Document]: + documents = [ + GraphDocument( + text=text, + graph_data=records + ) + ] + + return documents + + def _get_graph_schema_info(self) -> dict: + cypher = "CALL apoc.meta.schema() YIELD value RETURN value" + session = self.driver.session() + try: + result = session.run(cypher) + schema = [record["value"] for record in result] + if schema: + return schema[0] + return {} + finally: + session.close() + + def _llm_generate_cypher(self, natural_language_query: str, + schema_info: dict) -> str: + return "MATCH (n) RETURN n LIMIT 10" + + + def _build_node_ids_query(self, node_ids: List[int]) -> str: + ids_str = ", ".join(str(i) for i in node_ids) + return f"MATCH (n) WHERE id(n) IN [{ids_str}] RETURN n" + + + def insert_document(self, documents: List[Document], **kwargs: Any): + session = self.driver.session() + try: + for doc in documents: + cypher = f""" + MERGE (d:Document {{id: '{doc.id}'}}) + SET d.text = $text, + d.metadata = $metadata + """ + session.run(cypher, text=doc.text, metadata=doc.metadata) + finally: + session.close() + + + def upsert_document(self, documents: List[Document], **kwargs): + + self.insert_document(documents, **kwargs) + + def update_document(self, documents: List[Document], **kwargs): + session = self.driver.session() + try: + for doc in documents: + cypher = f""" + MATCH (d:Document {{id: '{doc.id}'}}) + SET d.text = $text, + d.metadata = $metadata + """ + session.run(cypher, text=doc.text, metadata=doc.metadata) + finally: + session.close() + + def _initialize_by_component_configer(self, + neo4j_store_configer: ComponentConfiger) -> 'Neo4jStore': + super()._initialize_by_component_configer(neo4j_store_configer) + if hasattr(neo4j_store_configer, "uri"): + self.uri = neo4j_store_configer.uri + if hasattr(neo4j_store_configer, "user"): + self.user = neo4j_store_configer.user + if hasattr(neo4j_store_configer, "password"): + self.password = neo4j_store_configer.password + if hasattr(neo4j_store_configer, "database"): + self.database = neo4j_store_configer.database + return self diff --git a/agentuniverse/agent/agent.py b/agentuniverse/agent/agent.py index f60ca5b1..09345aee 100644 --- a/agentuniverse/agent/agent.py +++ b/agentuniverse/agent/agent.py @@ -6,11 +6,13 @@ # @Email : lc299034@antgroup.com # @FileName: agent.py import json +import uuid from abc import abstractmethod, ABC from datetime import datetime +from threading import Thread from typing import Optional, Any, List -from langchain_core.runnables import RunnableSerializable +from langchain_core.runnables import RunnableSerializable, RunnableConfig from langchain_core.utils.json import parse_json_markdown from agentuniverse.agent.action.knowledge.knowledge import Knowledge @@ -26,6 +28,7 @@ from agentuniverse.agent.output_object import OutputObject from agentuniverse.agent.plan.planner.planner import Planner from agentuniverse.agent.plan.planner.planner_manager import PlannerManager +from agentuniverse.agent.plan.planner.react_planner.stream_callback import InvokeCallbackHandler from agentuniverse.base.annotation.trace import trace_agent from agentuniverse.base.component.component_base import ComponentBase from agentuniverse.base.component.component_enum import ComponentEnum @@ -34,8 +37,9 @@ from agentuniverse.base.config.component_configer.configers.agent_configer \ import AgentConfiger from agentuniverse.base.util.common_util import stream_output +from agentuniverse.base.context.framework_context_manager import FrameworkContextManager from agentuniverse.base.util.logging.logging_util import LOGGER -from agentuniverse.base.util.memory_util import generate_messages +from agentuniverse.base.util.memory_util import generate_messages, get_memory_string from agentuniverse.llm.llm import LLM from agentuniverse.llm.llm_manager import LLMManager from agentuniverse.prompt.chat_prompt import ChatPrompt @@ -229,14 +233,16 @@ def as_langchain_tool(self): ) def process_llm(self, **kwargs) -> LLM: - llm_name = kwargs.get('llm_name') or self.agent_model.profile.get('llm_model', {}).get('name') - return LLMManager().get_instance_obj(llm_name) + return LLMManager().get_instance_obj(self.llm_name) def process_memory(self, agent_input: dict, **kwargs) -> Memory | None: - memory_name = kwargs.get('memory_name') or self.agent_model.memory.get('name') - memory: Memory = MemoryManager().get_instance_obj(memory_name) - if memory is None: + memory: Memory = MemoryManager().get_instance_obj(component_instance_name=self.memory_name) + conversation_memory: Memory = MemoryManager().get_instance_obj( + component_instance_name=self.conversation_memory_name) + if memory is None and conversation_memory is None: return None + if memory is None: + memory = conversation_memory chat_history: list = agent_input.get('chat_history') # generate a list of temporary messages from the given chat history and add them to the memory instance. @@ -245,16 +251,16 @@ def process_memory(self, agent_input: dict, **kwargs) -> Memory | None: memory.add(temporary_messages, **agent_input) params: dict = dict() - params['agent_llm_name'] = kwargs.get('llm_name') or self.agent_model.profile.get('llm_model', {}).get('name') + params['agent_llm_name'] = self.llm_name return memory.set_by_agent_model(**params) def invoke_chain(self, chain: RunnableSerializable[Any, str], agent_input: dict, input_object: InputObject, **kwargs): if not input_object.get_data('output_stream'): - res = chain.invoke(input=agent_input) + res = chain.invoke(input=agent_input, config=self.get_run_config()) return res result = [] - for token in chain.stream(input=agent_input): + for token in chain.stream(input=agent_input, config=self.get_run_config()): stream_output(input_object.get_data('output_stream', None), { 'type': 'token', 'data': { @@ -268,10 +274,10 @@ def invoke_chain(self, chain: RunnableSerializable[Any, str], agent_input: dict, async def async_invoke_chain(self, chain: RunnableSerializable[Any, str], agent_input: dict, input_object: InputObject, **kwargs): if not input_object.get_data('output_stream'): - res = await chain.ainvoke(input=agent_input) + res = await chain.ainvoke(input=agent_input, config=self.get_run_config()) return res result = [] - async for token in chain.astream(input=agent_input): + async for token in chain.astream(input=agent_input, config=self.get_run_config()): stream_output(input_object.get_data('output_stream', None), { 'type': 'token', 'data': { @@ -283,13 +289,12 @@ async def async_invoke_chain(self, chain: RunnableSerializable[Any, str], agent_ return "".join(result) def invoke_tools(self, input_object: InputObject, **kwargs) -> str: - tool_names = kwargs.get('tool_names') or self.agent_model.action.get('tool', []) - if not tool_names: + if not self.tool_names: return '' tool_results: list = list() - for tool_name in tool_names: + for tool_name in self.tool_names: tool: Tool = ToolManager().get_instance_obj(tool_name) if tool is None: continue @@ -298,13 +303,12 @@ def invoke_tools(self, input_object: InputObject, **kwargs) -> str: return "\n\n".join(tool_results) def invoke_knowledge(self, query_str: str, input_object: InputObject, **kwargs) -> str: - knowledge_names = kwargs.get('knowledge_names') or self.agent_model.action.get('knowledge', []) - if not knowledge_names or not query_str: + if not self.knowledge_names or not query_str: return '' knowledge_results: list = list() - for knowledge_name in knowledge_names: + for knowledge_name in self.knowledge_names: knowledge: Knowledge = KnowledgeManager().get_instance_obj(knowledge_name) if knowledge is None: continue @@ -328,8 +332,7 @@ def process_prompt(self, agent_input: dict, **kwargs) -> ChatPrompt: instruction=profile_instruction) # get the prompt by the prompt version - prompt_version = kwargs.get('prompt_version') or self.agent_model.profile.get('prompt_version') - version_prompt: Prompt = PromptManager().get_instance_obj(prompt_version) + version_prompt: Prompt = PromptManager().get_instance_obj(self.prompt_version) if version_prompt is None and not profile_prompt_model: raise Exception("Either the `prompt_version` or `introduction & target & instruction`" @@ -346,3 +349,102 @@ def process_prompt(self, agent_input: dict, **kwargs) -> ChatPrompt: if image_urls: chat_prompt.generate_image_prompt(image_urls) return chat_prompt + + def get_memory_params(self, agent_input: dict) -> dict: + memory_info = self.agent_model.memory + memory_types = self.agent_model.memory.get('memory_types', None) + prune = self.agent_model.memory.get('prune', False) + top_k = self.agent_model.memory.get('top_k', 20) + session_id = agent_input.get('session_id') + agent_id = self.agent_model.info.get('name') + if not session_id: + session_id = FrameworkContextManager().get_context('session_id') + if "agent_id" in memory_info: + agent_id = memory_info.get('agent_id') + params = { + 'session_id': session_id, + 'agent_id': agent_id, + 'prune': prune, + 'top_k': top_k + } + if memory_types: + params['memory_types'] = memory_types + if agent_input.get('input'): + params['input'] = agent_input.get('input') + if not self.agent_model.memory.get('name') and self.agent_model.memory.get('conversation_memory'): + params["type"] = ['input', 'output'] + return params + + def get_run_config(self, **kwargs) -> dict: + callbacks = [InvokeCallbackHandler( + source=self.agent_model.info.get('name'), + llm_name=self.llm_name + )] + return RunnableConfig(callbacks=callbacks) + + def collect_current_memory(self, collect_type: str) -> bool: + collection_types = self.agent_model.memory.get('collection_types') + auto_trace = self.agent_model.memory.get('auto_trace', True) + if not auto_trace: + return False + if collection_types and collect_type not in collection_types: + return False + return True + + def load_memory(self, memory, agent_input: dict): + if memory: + params = self.get_memory_params(agent_input) + LOGGER.info(f"Load memory with params: {params}") + memory_messages = memory.get(**params) + memory_str = get_memory_string(memory_messages, agent_input.get('agent_id')) + else: + return "Up to Now, No Chat History" + agent_input[memory.memory_key] = memory_str + return memory_str + + def add_memory(self, memory: Memory, content: Any, type: str = 'Q&A', agent_input: dict[str, Any] = {}): + if not memory: + return + session_id = agent_input.get('session_id') + if not session_id: + session_id = FrameworkContextManager().get_context('session_id') + agent_id = self.agent_model.info.get('name') + message = Message(id=str(uuid.uuid4().hex), + source=agent_id, + content=content if isinstance(content, str) else json.dumps(content, ensure_ascii=False), + type=type, + metadata={ + 'agent_id': agent_id, + 'session_id': session_id, + 'type': type, + 'timestamp': datetime.now(), + 'gmt_created': datetime.now().isoformat() + }) + memory.add([message], session_id=session_id, agent_id=agent_id) + + def summarize_memory(self, agent_input: dict[str, Any] = {}, memory: Memory = None): + def do_summarize(params): + content = memory.summarize_memory(**params) + memory.add([ + Message( + id=str(uuid.uuid4().hex), + source=self.agent_model.info.get('name'), + content=content, + type='summarize' + ) + ], session_id=params['session_id'], agent_id=params['agent_id']) + + if memory: + params = self.get_memory_params(agent_input) + Thread(target=do_summarize, args=(params,)).start() + + def load_summarize_memory(self, memory: Memory, agent_input: dict[str, Any] = {}) -> str: + if memory: + params = self.get_memory_params(agent_input) + params['type'] = 'summarize' + memory_messages = memory.get(**params) + if len(memory_messages) == 0: + return "Up to Now, No Summarize Memory" + else: + return memory_messages[-1].content + return "Up to Now, No Summarize Memory" diff --git a/agentuniverse/agent/default/rag_agent/memory_summarize_agent.yaml b/agentuniverse/agent/default/rag_agent/memory_summarize_agent.yaml new file mode 100644 index 00000000..c7356300 --- /dev/null +++ b/agentuniverse/agent/default/rag_agent/memory_summarize_agent.yaml @@ -0,0 +1,15 @@ +info: + name: 'memory_summarize_agent' + description: 'memory summarize agent' +profile: + prompt_version: 'memory_summarize_cn_prompt' + llm_model: + name: 'deep_seek_llm' +# model_name: 'qwen2.5-72b-instruct' + temperature: 0.7 +memory: + auto_trace: false +metadata: + type: 'AGENT' + module: 'agentuniverse.agent.template.default_summarize_agent_template' + class: 'SummarizeRagAgentTemplate' \ No newline at end of file diff --git a/agentuniverse/agent/default/rag_agent/memory_summarize_cn_prompt.yaml b/agentuniverse/agent/default/rag_agent/memory_summarize_cn_prompt.yaml new file mode 100644 index 00000000..52e7f49d --- /dev/null +++ b/agentuniverse/agent/default/rag_agent/memory_summarize_cn_prompt.yaml @@ -0,0 +1,23 @@ +introduction: 你是一位精通信息分析的ai助手。 +target: 请仅基于以下聊天记录的内容,提供一份精简且准确的总结。总结应包含聊天的主要议题和结论,不要包括与总结无关的信息或评论。 +instruction: | + 请分析以下聊天记录,并基于先前的记忆概要。提取并回答以下问题: + + 1. 用户的主要意向是什么? + 2. 之前聊天的内容摘要是什么? + + 回答要求: + - 对于每个问题,提供一个简洁明了的答案。 + - 在概括聊天内容时,请确保包括关键点和讨论的主题,但不需要逐字重复。 + - 总结应聚焦于信息的核心,而不是对话中的每一个细节。 + - 确保意向的提取直接反映了用户的意图,而不过度解释或推测。 + + 之前的聊天记录: + {input} + + 上一次总结的结果: + {summarize_content} + +metadata: + type: 'PROMPT' + version: 'memory_summarize_cn_prompt' \ No newline at end of file diff --git a/agentuniverse/agent/memory/conversation_memory/__init__.py b/agentuniverse/agent/memory/conversation_memory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentuniverse/agent/memory/conversation_memory/conversation_memory_module.py b/agentuniverse/agent/memory/conversation_memory/conversation_memory_module.py new file mode 100644 index 00000000..8535e433 --- /dev/null +++ b/agentuniverse/agent/memory/conversation_memory/conversation_memory_module.py @@ -0,0 +1,364 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/11/21 16:55 +# @Author : weizjajj +# @Email : weizhongjie.wzj@antgroup.com +# @FileName: trace_memory.py + +import datetime +import json +import queue +import traceback +import uuid +from concurrent.futures.thread import ThreadPoolExecutor +from threading import Thread +from typing import List, Optional + +from agentuniverse.agent.agent_manager import AgentManager + +from agentuniverse.agent.action.knowledge.store.document import Document +from agentuniverse.agent.memory.conversation_memory.conversation_message import ConversationMessage +from agentuniverse.agent.memory.conversation_memory.enum import ConversationMessageSourceType +from agentuniverse.agent.memory.memory_manager import MemoryManager +from agentuniverse.agent.output_object import OutputObject +from agentuniverse.base.annotation.singleton import singleton +from agentuniverse.base.config.application_configer.application_config_manager import ApplicationConfigManager +from agentuniverse.base.context.framework_context_manager import FrameworkContextManager +from agentuniverse.base.util.logging.logging_util import LOGGER + + +def generate_relation_str(source: str, target: str, source_type: str, target_type: str, type: str): + if source_type == 'agent' and target_type == 'agent' and type == 'input': + return f"智能体 {source} 向智能体 {target} 提出了一个问题" + if source_type == 'agent' and target_type == 'agent' and type == 'output': + return f"智能体 {target} 回答了智能体 {source} 的问题" + if source_type == 'agent' and target_type == 'tool' and type == 'input': + return f"智能体 {source} 调用了工具 {target},执行的参数是" + if source_type == 'agent' and target_type == 'tool' and type == 'output': + return f"工具 {target} 返回给智能体 {source} 的执行结果" + if source_type == 'agent' and target_type == 'knowledge' and type == 'input': + return f"智能体 {source} 在知识库 {target} 中进行了搜索,关键词是" + if source_type == 'agent' and target_type == 'knowledge' and type == 'output': + return f"知识库 {target} 返回给智能体 {source} 的搜索结果" + if source_type == 'agent' and target_type == 'llm' and type == 'input': + return f"智能体 {source} 向大模型 {target} 提问" + if source_type == 'agent' and target_type == 'llm' and type == 'output': + return f"大模型 {target} 返回给智能体 {source} 的答案" + if source_type == 'unknown' and target_type == 'agent' and type == 'input': + return f"未知类型 {source} 向智能体 {target} 提出了一个问题" + if source_type == 'unknown' and target_type == 'agent' and type == 'output': + return f"智能体 {target} 回答了未知 {source} 的问题" + if source_type == "user" and target_type == 'agent' and type == 'input': + return f"用户向智能体 {target} 提出了一个问题" + if source_type == 'user' and target_type == 'agent' and type == 'output': + return f"智能体 {target} 回答了用户的问题" + elif type == 'input': + return f"{source} 向 {target} 询问了一个问题" + elif type == 'output': + return f"{source} 回答了 {target} 的问题" + elif type == 'summary': + return f"{source} 的摘要" + return None + + +def generate_relation_str_en(source: str, target: str, source_type: str, target_type: str, type: str): + if source_type == 'agent' and target_type == 'agent' and type == 'input': + return f"Agent {source} asked a question to agent {target}" + if source_type == 'agent' and target_type == 'agent' and type == 'output': + return f"Agent {target} answered the question asked by agent {source}" + if source_type == 'agent' and target_type == 'tool' and type == 'input': + return f"Agent {source} called tool {target}, the parameters are" + if source_type == 'agent' and target_type == 'tool' and type == 'output': + return f"Tool {target} returned the result to agent {source}" + if source_type == 'agent' and target_type == 'knowledge' and type == 'input': + return f"Agent {source} searched in knowledge {target}, the keywords are" + if source_type == 'agent' and target_type == 'knowledge' and type == 'output': + return f"Knowledge {target} returned the result to agent {target}" + if source_type == 'agent' and target_type == 'llm' and type == 'input': + return f"Agent {source} asked a question to llm {target}" + if source_type == 'agent' and target_type == 'llm' and type == 'output': + return f"LLM {target} returned the answer to agent {source}" + if source_type == 'unknown' and target_type == 'agent' and type == 'input': + return f"Unknown type {source} asked a question to agent {target}" + if source_type == 'unknown' and target_type == 'agent' and type == 'output': + return f"Agent {target} answered the unknown {source} question" + if source_type == "user" and target_type == 'agent' and type == 'input': + return f"User asked a question to agent {target}" + if source_type == 'user' and target_type == 'agent' and type == 'output': + return f"Agent {target} answered the user's question" + if type == 'input': + return f"{source} asked a question to {target}" + elif type == 'output': + return f"{target} answered {source}'s question" + elif type == 'summary': + return f"{source} summary" + return None + + +def sync_to_sub_agent_memory(message: ConversationMessage, session_id: str, memory_name: str): + def add_message(agent_name: str, memory_names: list, collect_type: str): + agent_instance = AgentManager().get_instance_obj(agent_name) + agent_memory = agent_instance.agent_model.memory.get('conversation_memory') + collection_types = agent_instance.agent_model.memory.get('collection_types') + if collection_types and collect_type not in collection_types: + return + if agent_memory: + memory_instance = MemoryManager().get_instance_obj(agent_memory) + memory_instance.add([message], session_id=session_id) + memory_names.append(agent_memory) + + memory_names = [memory_name] + if message.source_type == ConversationMessageSourceType.AGENT.value: + add_message(message.source, memory_names, message.target_type) + + if message.target_type == ConversationMessageSourceType.AGENT.value: + add_message(message.target, memory_names, message.source_type) + + +@singleton +class ConversationMemoryModule: + + def __init__(self): + conversation_memory_configer = ApplicationConfigManager().app_configer.conversation_memory_configer + self.instance_name = conversation_memory_configer.get('instance_name', '') + self.activate = conversation_memory_configer.get('activate', False) + self.logging = conversation_memory_configer.get('logging', False) + self.collection_types = conversation_memory_configer.get('collection_types', ['agent', 'user']) + self.conversation_format = conversation_memory_configer.get('conversation_format', 'cn') + self.max_content_length = conversation_memory_configer.get('max_content_length', 8000) + self.queue = queue.Queue(1000) + self.thread_pool = ThreadPoolExecutor(max_workers=conversation_memory_configer.get('thread_pool', 4)) + Thread(target=self._consume_queue, daemon=True).start() + + def _consume_queue(self): + while True: + func = self.queue.get() + try: + self.thread_pool.submit(func) + except Exception as e: + LOGGER.error(f"Failed to process trace info: {e}") + # 打印详细堆栈信息 + traceback.print_exc() + finally: + self.queue.task_done() + + def _add_trace_info(self, source: str, + source_type: str, + target: str, + target_type: str, + type: str, + params: dict, **kwargs) -> None: + if not self.activate: + return + content = None + if type == "input" and target_type == 'agent': + agent_instance = AgentManager().get_instance_obj(target) + input_field = agent_instance.agent_model.memory.get('input_field') + if input_field and input_field in params: + content = params.get(input_field) + elif type == "output" and source_type == 'agent': + agent_instance = AgentManager().get_instance_obj(source) + output_field = agent_instance.agent_model.memory.get('output_field') + if output_field and output_field in params: + content = params.get(output_field) + + if content is None and type in params: + content = params[type] + elif content is None: + try: + content = json.dumps(params, ensure_ascii=False) + except Exception as e: + content = str(e) + try: + params_json = json.dumps(params, ensure_ascii=False) + except Exception as e: + params_json = json.dumps({ + "error": str(e) + }, ensure_ascii=False) + if self.conversation_format == 'cn': + prefix = generate_relation_str(source, target, source_type, target_type, type) + else: + prefix = generate_relation_str_en(source, target, source_type, target_type, type) + if not prefix: + return + if isinstance(content, str) and len(content) > self.max_content_length: + content = content[:self.max_content_length] + if len(params_json) > self.max_content_length: + params_json = params_json[:self.max_content_length] + if self.logging: + LOGGER.info( + f"{kwargs.get('session_id')} | {kwargs.get('trace_id')}| {kwargs.get('pair_id')} |\n {prefix}:{content}") + message = ConversationMessage( + id=uuid.uuid4().hex, + conversation_id=kwargs.get('session_id'), + trace_id=kwargs.get('trace_id'), + source=source, + source_type=source_type, + target=target, + target_type=target_type, + type=type, + metadata={ + "timestamp": datetime.datetime.now(), + "prefix": prefix, + "params": params_json, + "pair_id": kwargs.get('pair_id'), + 'gmt_created': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + }, + content=f"{content}" + ) + if self.instance_name: + memory = MemoryManager().get_instance_obj(self.instance_name) + if memory: + memory.add([message], session_id=kwargs.get('session_id')) + sync_to_sub_agent_memory(message, kwargs.get('session_id'), self.instance_name) + + def _add_trace(self, start_info, target_info: dict, type: str, params: dict, session_id: str, trace_id: str, + pair_id: str): + if "kwargs" in params: + params = params['kwargs'] + if params is str: + params = { + type: params + } + + kwargs = {'source': start_info['source'], 'source_type': start_info['type'], 'target': target_info['source'], + 'target_type': target_info['type'], 'type': type, 'params': params, 'trace_id': trace_id, + 'session_id': session_id, + "pair_id": pair_id} + self._add_trace_info(**kwargs) + + def add_trace_info(self, start_info: dict, target_info: dict, type: str, params: dict, pair_id: str): + """Add trace info to the memory.""" + trace_id = FrameworkContextManager().get_context('trace_id') + if trace_id is None: + trace_id = str(uuid.uuid4()) + FrameworkContextManager().set_context('trace_id', trace_id) + + session_id = FrameworkContextManager().get_context('session_id') + if session_id is None: + session_id = str(uuid.uuid4()) + FrameworkContextManager().set_context('session_id', session_id) + + def add_trace(): + self._add_trace(start_info, target_info, type, params, session_id, + trace_id, pair_id) + + self.queue.put_nowait(add_trace) + + def add_tool_input_info(self, start_info: dict, target: str, params: dict, pair_id: str, auto: bool = True): + """Add trace info to the memory.""" + + if not self.collection_current_agent_memory(start_info, 'tool', auto): + return + + target_info = {'source': target, 'type': 'tool'} + self.add_trace_info(start_info, target_info, 'input', params, pair_id) + + def add_tool_output_info(self, start_info: dict, target: str, params: dict, pair_id: str, auto: bool = True): + """Add trace info to the memory.""" + if not self.collection_current_agent_memory(start_info, 'tool', auto): + return + + target_info = {'source': target, 'type': 'tool'} + self.add_trace_info(start_info, target_info, 'output', params, pair_id) + + def add_knowledge_input_info(self, start_info: dict, target: str, params: dict, pair_id: str, auto: bool = True): + + if not self.collection_current_agent_memory(start_info, 'knowledge', auto): + return + + target_info = {'source': target, 'type': 'knowledge'} + self.add_trace_info(start_info, target_info, 'input', params, pair_id) + + def add_knowledge_output_info(self, start_info: dict, target: str, params: List[Document], pair_id: str, + auto: bool = True): + + if not self.collection_current_agent_memory(start_info, 'knowledge', auto): + return + target_info = {'source': target, 'type': 'knowledge'} + doc_data = [] + for doc in params: + doc_data.append(doc.text) + self.add_trace_info(start_info, target_info, 'output', { + 'output': "\n==============================\n".join(doc_data) + }, pair_id) + + def add_agent_input_info(self, start_info: dict, instance: 'Agent', params: dict, pair_id: str, + auto: bool = True): + if auto: + if not instance.collect_current_memory(start_info.get('type')): + return + if not self.activate: + return + if 'agent' not in self.collection_types: + return + + target_info = {'source': instance.agent_model.info.get('name'), 'type': 'agent'} + input_keys = instance.input_keys() + if "kwargs" in params: + params: dict = params['kwargs'] + params = params.copy() + params.pop('output_stream') if 'output_stream' in params else params + if auto: + params = {key: params[key] for key in input_keys} + self.add_trace_info(start_info, target_info, 'input', params, pair_id) + + def add_agent_result_info(self, agent_instance: 'Agent', agent_result: Optional[OutputObject | dict], + target_info: dict, + pair_id: str, auto: bool = True): + + if auto: + if not agent_instance.collect_current_memory(target_info.get('type')): + return + + trace_id = FrameworkContextManager().get_context('trace_id') + session_id = FrameworkContextManager().get_context('session_id') + + def add_trace(): + output_keys = agent_instance.output_keys() + if auto: + params = {key: agent_result.get_data(key) for key in output_keys} + else: + params = agent_result + start_info = { + "source": agent_instance.agent_model.info.get('name'), + "type": "agent" + } + self._add_trace(target_info, start_info, 'output', params, session_id, trace_id, pair_id) + + self.queue.put_nowait(add_trace) + + def add_llm_input_info(self, start_info: dict, target: str, prompt: str, pair_id: str, auto=True): + if not self.collection_current_agent_memory(start_info, 'llm', auto): + return + + target_info = {'source': target, 'type': 'llm'} + self.add_trace_info(start_info, target_info, 'input', {'input': prompt}, pair_id) + + def add_llm_output_info(self, start_info: dict, target: str, output: str, pair_id: str, auto=True): + if not self.collection_current_agent_memory(start_info, 'llm', auto): + return + target_info = {'source': target, 'type': 'llm'} + self.add_trace_info(start_info, target_info, 'output', { + 'output': output + }, pair_id) + + def collection_current_agent_memory(self, info: dict, collection_type: str, auto: bool): + if not auto: + return True + if not self.activate: + return False + if info.get('type') == 'agent': + agent_id = info.get('source') + agent_instance = AgentManager().get_instance_obj(agent_id) + if agent_instance: + collection_types = agent_instance.agent_model.memory.get('collection_types') + res = agent_instance.collect_current_memory(collection_type) + if not res: + return False + if collection_types: + return res + if collection_type not in self.collection_types: + return False + return True diff --git a/agentuniverse/agent/memory/conversation_memory/conversation_message.py b/agentuniverse/agent/memory/conversation_memory/conversation_message.py new file mode 100644 index 00000000..dbfe5fdc --- /dev/null +++ b/agentuniverse/agent/memory/conversation_memory/conversation_message.py @@ -0,0 +1,114 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/12/5 17:43 +# @Author : weizjajj +# @Email : weizhongjie.wzj@antgroup.com +# @FileName: conversation_message.py + +import uuid +from typing import Optional, List + +from pydantic import Field + +from agentuniverse.agent.memory.enum import ChatMessageEnum +from langchain_core.prompts import HumanMessagePromptTemplate, AIMessagePromptTemplate +from langchain_core.prompts.chat import BaseStringMessagePromptTemplate + +from agentuniverse.agent.memory.conversation_memory.enum import ConversationMessageSourceType, ConversationMessageEnum +from agentuniverse.agent.memory.message import Message +from agentuniverse.base.context.framework_context_manager import FrameworkContextManager + + +class ConversationMessage(Message): + """ + The basic class for conversation memory message + + Attributes: + id (Optional[str]): Unique identifier. + trace_id (Optional[str]): Trace ID. + conversation_id (Optional[str]): Conversation ID. + source (Optional[str]): Message source. + source_type (Optional[str]): Type of the message source. + target (Optional[str]): Message target. + target_type (Optional[str]): Type of the message target. + type (Optional[str]): Message type. + content (Optional[str]): Message content. + metadata (Optional[dict]): The metadata of the message. + """ + id: Optional[str | int] = uuid.uuid4().hex + trace_id: Optional[str] = None + conversation_id: Optional[str] = None + source: Optional[str] = None + source_type: Optional[str] = None + target: Optional[str] = None + target_type: Optional[str] = None + type: Optional[str] = None + content: Optional[str] = None + metadata: Optional[dict] = None + additional_args: Optional[dict] = Field(default_factory=dict) + + @staticmethod + def as_langchain_list(message_list: List['ConversationMessage']): + """Convert agentUniverse(aU) message list to langchain message list """ + messages = [] + for message in message_list: + # only got agent message + if message.target_type != ConversationMessageSourceType.AGENT.value: + continue + if message.source_type not in [ConversationMessageSourceType.AGENT.value, + ConversationMessageSourceType.USER.value]: + continue + if message.source_type == ConversationMessageSourceType.AGENT.value and message.type == ConversationMessageEnum.OUTPUT.value: + messages.append(message) + elif message.target_type == ConversationMessageSourceType.AGENT.value and message.type == ConversationMessageEnum.INPUT.value: + messages.append(message) + return [message.as_langchain() for message in messages] + + def as_langchain(self): + """Convert the agentUniverse(aU) message class to the langchain message class.""" + if self.type in [ConversationMessageSourceType.AGENT.value, + ConversationMessageSourceType.USER.value]: + return HumanMessagePromptTemplate.from_template(self.content) + elif self.type == ChatMessageEnum.AI.value: + return AIMessagePromptTemplate.from_template(self.content) + else: + return BaseStringMessagePromptTemplate.from_template(self.content) + + @classmethod + def from_dict(cls, data: dict): + """Convert the agentUniverse(aU) message class to the dict.""" + return cls(**data) + + @classmethod + def from_message(cls, message: Message, session_id: str): + if not message.metadata: + message.metadata = {} + message.metadata['prefix'] = '之前对话的摘要:' if message.type == 'summarize' else '' + message.metadata['params'] = "{}" + trace_id = message.metadata.get('trace_id') + if not trace_id: + trace_id = FrameworkContextManager().get_context('trace_id') + message.metadata['trace_id'] = trace_id + return cls( + id=uuid.uuid4().hex, + content=message.content, + metadata=message.metadata, + type=message.type, + source=message.source, + source_type='agent', + target=message.source, + target_type='agent', + trace_id=trace_id, + conversation_id=message.metadata.get('session_id') if not session_id else session_id, + ) + + @classmethod + def check_and_convert_message(cls, messages, session_id: str = None): + if len(messages) == 0: + return [] + message = messages[0] + if isinstance(message, cls): + return messages + if isinstance(message, Message): + return [cls.from_message(m, session_id) for m in messages] diff --git a/agentuniverse/agent/memory/conversation_memory/enum.py b/agentuniverse/agent/memory/conversation_memory/enum.py new file mode 100644 index 00000000..7c5aab11 --- /dev/null +++ b/agentuniverse/agent/memory/conversation_memory/enum.py @@ -0,0 +1,25 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/3/15 11:42 +# @Author : wangchongshi +# @Email : wangchongshi.wcs@antgroup.com +# @FileName: enum.py + +import enum +from enum import Enum + + +@enum.unique +class ConversationMessageEnum(Enum): + INPUT = 'input' + OUTPUT = 'output' + + +@enum.unique +class ConversationMessageSourceType(Enum): + AGENT = 'agent' + TOOL = 'tool' + KNOWLEDGE = 'knowledge' + LLM = 'llm' + USER = 'user' diff --git a/agentuniverse/agent/memory/conversation_memory/memory_storage/__init__.py b/agentuniverse/agent/memory/conversation_memory/memory_storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agentuniverse/agent/memory/conversation_memory/memory_storage/chroma_conversation_memory_storage.py b/agentuniverse/agent/memory/conversation_memory/memory_storage/chroma_conversation_memory_storage.py new file mode 100644 index 00000000..88b23a54 --- /dev/null +++ b/agentuniverse/agent/memory/conversation_memory/memory_storage/chroma_conversation_memory_storage.py @@ -0,0 +1,280 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- +import json +# @Time : 2024/10/10 19:10 +# @Author : wangchongshi +# @Email : wangchongshi.wcs@antgroup.com +# @FileName: chroma_memory_storage.py + +import uuid +from datetime import datetime +from urllib.parse import urlparse +from typing import Optional, List, Any + +import chromadb +from pydantic import SkipValidation +from chromadb.config import Settings +from chromadb.api.models.Collection import Collection + +from agentuniverse.agent.action.knowledge.embedding.embedding_manager import EmbeddingManager +from agentuniverse.agent.memory.conversation_memory.conversation_message import ConversationMessage +from agentuniverse.agent.memory.conversation_memory.enum import ConversationMessageEnum, ConversationMessageSourceType +from agentuniverse.agent.memory.memory_storage.memory_storage import MemoryStorage +from agentuniverse.base.config.component_configer.component_configer import ComponentConfiger + + +class ChromaConversationMemoryStorage(MemoryStorage): + """The chroma memory storage class. + + Attributes: + collection_name (str): The name of the ChromaDB collection. + persist_path (str): The path to persist the collection. + embedding_model (str): The name of the embedding model instance to use. + _collection (Collection): The collection object. + """ + collection_name: Optional[str] = 'memory' + persist_path: Optional[str] = None + embedding_model: Optional[str] = None + _collection: SkipValidation[Collection] = None + + def _initialize_by_component_configer(self, + memory_storage_config: ComponentConfiger) -> 'ChromaMemoryStorage': + """Initialize the ChromaMemoryStorage by the ComponentConfiger object. + + Args: + memory_storage_config(ComponentConfiger): A configer contains chroma_memory_storage basic info. + Returns: + ChromaMemoryStorage: A ChromaMemoryStorage instance. + """ + super()._initialize_by_component_configer(memory_storage_config) + if getattr(memory_storage_config, 'collection_name', None): + self.collection_name = memory_storage_config.collection_name + if getattr(memory_storage_config, 'persist_path', None): + self.persist_path = memory_storage_config.persist_path + if getattr(memory_storage_config, 'embedding_model', None): + self.embedding_model = memory_storage_config.embedding_model + return self + + def _init_collection(self) -> Any: + """Initialize the ChromaDB collection.""" + if self.persist_path.startswith('http') or self.persist_path.startswith('https'): + parsed_url = urlparse(self.persist_path) + settings = Settings( + chroma_api_impl="chromadb.api.fastapi.FastAPI", + chroma_server_host=parsed_url.hostname, + chroma_server_http_port=str(parsed_url.port) + ) + else: + settings = Settings( + is_persistent=True, + persist_directory=self.persist_path + ) + client = chromadb.Client(settings) + self._collection = client.get_or_create_collection(name=self.collection_name) + return client + + def delete(self, session_id: str = None, agent_id: str = None, **kwargs) -> None: + """Delete the memory from the database. + + Args: + session_id (str): The session id of the memory to delete. + agent_id (str): The agent id of the memory to delete. + """ + if self._collection is None: + self._init_collection() + filters = {} + if session_id is None and agent_id is None: + return + if session_id is not None: + filters['session_id'] = session_id + if agent_id is not None: + filters['agent_id'] = agent_id + self._collection.delete(where=filters) + + def add(self, message_list: List[ConversationMessage], session_id: str = None, trace_id: str = None, + **kwargs) -> None: + """Add messages to the memory db. + + Args: + message_list (List[Message]): The list of messages to add. + session_id (str): The session id of the memory to add. + agent_id (str): The agent id of the memory to add. + """ + message_list = ConversationMessage.check_and_convert_message(message_list, session_id) + if self._collection is None: + self._init_collection() + if not message_list: + return + for message in message_list: + embedding = [] + if self.embedding_model: + embedding = EmbeddingManager().get_instance_obj( + self.embedding_model + ).get_embeddings([message.content])[0] + metadata = {'timestamp': datetime.now().isoformat()} + if session_id: + metadata['session_id'] = session_id + metadata['trace_id'] = message.trace_id + metadata['source'] = message.source + metadata['source_type'] = message.source_type if message.source_type else '' + metadata['target'] = message.target + metadata['target_type'] = message.target_type if message.target_type else '' + metadata['type'] = message.type if message.type else '' + metadata['session_id'] = session_id if session_id else message.conversation_id + metadata['params'] = message.metadata.get('params') + metadata['prefix'] = message.metadata.get('prefix') + metadata['pair_id'] = message.metadata.get('pair_id') + metadata['additional_args'] = json.dumps(message.additional_args) + self._collection.add( + ids=[message.id if message.id else str(uuid.uuid4())], + documents=[message.content], + metadatas=[metadata], + embeddings=[embedding] if len(embedding) > 0 else None, + ) + + def get(self, session_id: str = None, agent_id: str = None, top_k=50, input: str = '', **kwargs) -> \ + List[ConversationMessage]: + """Get messages from the memory db. + + Args: + session_id (str): The session id of the memory to get. + agent_id (str): The agent id of the memory to get. + top_k (int): The number of messages to return. + input (str): The input text to search for in the memory. + source (str): The source of the message to get. + Returns: + List[Message]: A list of messages retrieved from the memory db. + """ + if self._collection is None: + self._init_collection() + filters = {"$and": []} + if session_id: + filters["$and"].append({'session_id': session_id}) + + if 'type' in kwargs: + if isinstance(kwargs['type'], list): + types = kwargs['type'] + elif isinstance(kwargs['type'], str): + types = [kwargs['type']] + else: + raise ValueError("type must be a list or str") + filters["$and"].append({'type': { + "$in": types + }}) + + if agent_id: + condition = { + "$and": [ + {'target': agent_id}, + {'target_type': ConversationMessageSourceType.AGENT.value} + ] + } + if kwargs['memory_types'] and len(kwargs["memory_types"]) > 0: + condition = { + "$or": [ + condition, + { + "$and": [ + {'source': agent_id}, + {'source_type': ConversationMessageSourceType.AGENT.value}, + {'target_type': { + "$in": kwargs["memory_types"] + }} + ] + } + ] + } + filters["$and"].append(condition) + + if 'trace_id' in kwargs: + filters["$and"].append({'trace_id': kwargs['trace_id']}) + + if len(filters["$and"]) < 2: + filters = filters["$and"][0] if len(filters["$and"]) == 1 else {} + input = None + if input: + embedding = [] + if self.embedding_model: + embedding = EmbeddingManager().get_instance_obj( + self.embedding_model + ).get_embeddings([input])[0] + if len(embedding) > 0: + results = self._collection.query( + query_embeddings=embedding, where=filters, n_results=top_k + ) + else: + results = self._collection.query(query_texts=[input], where=filters, n_results=top_k) + messages = self.to_messages(result=results) + messages.reverse() + return messages + else: + results = self._collection.get(where=filters) + messages = self.to_messages(result=results, sort_by_time=True) + return messages[-top_k:] + + def to_messages(self, result: dict, sort_by_time: bool = False) -> List[ConversationMessage]: + """Convert the result from ChromaDB to a list of aU messages. + + Args: + result (dict): The result from ChromaDB. + sort_by_time (bool): Whether to sort the messages by time. + Returns: + List[Message]: A list of aU messages. + """ + message_list = [] + if not result or not result['ids']: + return message_list + try: + if self.is_nested_list(result['ids']): + metadatas = result.get('metadatas', [[]]) + documents = result.get('documents', [[]]) + ids = result.get('ids', [[]]) + message_list = [ + ConversationMessage( + id=ids[0][i], + conversation_id=metadatas[0][i].get('session_id', None) if metadatas[0] else None, + content=documents[0][i], + metadata=metadatas[0][i] if metadatas[0] else None, + source=metadatas[0][i].get('source', None) if metadatas[0] else None, + source_type=metadatas[0][i].get('source_type', ''), + target=metadatas[0][i].get('target', None) if metadatas[0] else None, + target_type=metadatas[0][i].get('target_type', '') if metadatas[0] else '', + trace_id=metadatas[0][i].get('trace_id', '') if metadatas[0] else '', + type=metadatas[0][i].get('type', '') if metadatas[0] else '', + additional_args=json.loads(metadatas[0][i].get('additional_args', "{}")) + ) + for i in range(len(result['ids'][0])) + ] + else: + metadatas = result.get('metadatas', []) + documents = result.get('documents', []) + ids = result.get('ids', []) + message_list = [ + ConversationMessage( + id=ids[i], + conversation_id=metadatas[i].get('session_id', None) if metadatas[i] else None, + source_type=metadatas[i].get('source_type', ''), + target_type=metadatas[i].get('target_type', '') if metadatas[i] else '', + target=metadatas[i].get('target', None) if metadatas[i] else None, + trace_id=metadatas[i].get('trace_id', '') if metadatas[i] else '', + source=metadatas[i].get('source', None) if metadatas[i] else None, + content=documents[i], + metadata=metadatas[i] if metadatas[i] else None, + type=metadatas[i].get('type', '') if metadatas[i] else '', + additional_args=json.loads(metadatas[i].get('additional_args', "{}")) + ) + for i in range(len(result['ids'])) + ] + if sort_by_time: + # order by timestamp asc + message_list = sorted( + message_list, + key=lambda msg: msg.metadata.get('timestamp', ''), + ) + except Exception as e: + print('ChromaMemory.to_messages failed, exception= ' + str(e)) + return message_list + + @staticmethod + def is_nested_list(variable: List) -> bool: + return isinstance(variable, list) and len(variable) > 0 and isinstance(variable[0], list) diff --git a/agentuniverse/agent/memory/conversation_memory/memory_storage/es_conversation_memory_storage.py b/agentuniverse/agent/memory/conversation_memory/memory_storage/es_conversation_memory_storage.py new file mode 100644 index 00000000..2e93e14a --- /dev/null +++ b/agentuniverse/agent/memory/conversation_memory/memory_storage/es_conversation_memory_storage.py @@ -0,0 +1,299 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- +# @Time : 2024/12/13 11:19 +# @Author : weizjajj +# @Email : weizhongjie.wzj@antgroup.com +# @FileName: es_conversation_memory_storage.py +import json +import datetime + +import httpx +from typing import Optional, List, Any +from agentuniverse.agent.memory.conversation_memory.conversation_message import ConversationMessage +from agentuniverse.agent.memory.memory_storage.memory_storage import MemoryStorage +from agentuniverse.agent.memory.memory_storage.sql_alchemy_memory_storage import BaseMemoryConverter +from agentuniverse.agent.memory.message import Message +from agentuniverse.base.config.component_configer.component_configer import ComponentConfiger + + +class ElasticsearchMemoryStorage(MemoryStorage): + """ElasticsearchMemoryStorage class that stores messages via HTTP in an Elasticsearch index. + + Attributes: + es_url (str): The base URL of the Elasticsearch server. + index_name (str): The name of the Elasticsearch index to store messages. + memory_converter (BaseMemoryConverter): The memory converter to use for the memory. + """ + + es_url: Optional[str] = 'http://localhost:9200' # The base URL of your Elasticsearch instance + index_name: Optional[str] = 'memory' + memory_converter: BaseMemoryConverter = None + user: Optional[str] = None + password: Optional[str] = None + timeout: Optional[int] = 60 + client: Optional[httpx.Client] = None + + model_config = { + "arbitrary_types_allowed": True, # Allow arbitrary types + } + + def _new_client(self): + """Initialize the Elasticsearch HTTP client (via requests).""" + self.client = self._client() + self._init_es_index() + + def _initialize_by_component_configer(self, + memory_storage_config: ComponentConfiger) -> 'ElasticsearchMemoryStorage': + """Initialize the ElasticsearchMemoryStorage by the ComponentConfiger object.""" + super()._initialize_by_component_configer(memory_storage_config) + if getattr(memory_storage_config, 'es_url', None): + self.es_url = memory_storage_config.es_url + if getattr(memory_storage_config, 'es_index_name', None): + self.index_name = memory_storage_config.es_index_name + if getattr(memory_storage_config, 'es_user', None): + self.user = memory_storage_config.es_user + if getattr(memory_storage_config, 'es_password', None): + self.password = memory_storage_config.es_password + if getattr(memory_storage_config, 'es_timeout', None): + self.timeout = memory_storage_config.es_timeout + if self.es_url is None: + raise Exception('`es_url` is not set') + # initialize the memory converter if not set + if self.memory_converter is None: + self.memory_converter = DefaultMemoryConverter(self.index_name) + self._new_client() + return self + + def _init_es_index(self): + """Create the Elasticsearch index if it does not exist.""" + response = self.client.get( + f'/{self.index_name}' + ) + if response.status_code == 404: # Index does not exist, create it + settings = { + "settings": { + "number_of_shards": 1, + "number_of_replicas": 0 + }, + "mappings": { + "properties": { + "session_id": {"type": "keyword"}, + "content": {"type": "text"}, + "trace_id": {"type": "keyword"}, + "source": {"type": "keyword"}, + "source_type": {"type": "keyword"}, + "target": {"type": "keyword"}, + "target_type": {"type": "keyword"}, + "type": {"type": "keyword"}, + "prefix": {"type": "text"}, + "timestamp": {"type": "date"}, + "params": {"type": "text"}, + "pair_id": {"type": "keyword"}, + "additional_args": {"type": "object"} + } + } + } + response = self.client.put( + f'/{self.index_name}', + json=settings + ) + if response.status_code != 200: + raise Exception(f"Failed to create index: {response.text}") + + def delete(self, session_id: str = None, agent_id: str = None, trace_id: str = None, **kwargs) -> None: + """Delete the memory from Elasticsearch. + + Args: + session_id (str): The session id of the memory to delete. + agent_id (str): The agent id of the memory to delete. + """ + url = f'{self.es_url}/{self.index_name}/_delete_by_query' + query = { + "query": { + "bool": { + "must": [] + } + } + } + + if session_id: + query['query']['bool']['must'].append({"term": {"session_id": session_id}}) + if agent_id: + query['query']['bool']['must'].append({ + "bool": { + "should": [ + {"term": {"source": agent_id}}, + {"term": {"target": agent_id}} + ] + } + }) + if trace_id: + query['query']['bool']['must'].append({"term": {"trace_id": trace_id}}) + response = self.client.post(url, json=query) + if response.status_code != 200: + raise Exception(f"Failed to delete documents: {response.text}") + + def add(self, message_list: List[ConversationMessage], session_id: str = None, agent_id: str = None, + **kwargs) -> None: + """Add messages to the Elasticsearch index. + + Args: + message_list (List[Message]): The list of messages to add. + session_id (str): The session id of the memory to add. + agent_id (str): The agent id of the memory to add. + """ + message_list = ConversationMessage.check_and_convert_message(message_list, session_id) + actions = [] + for message in message_list: + action = self.memory_converter.to_es_action(message, session_id=session_id, agent_id=agent_id, **kwargs) + actions.append(action) + + bulk_data = '\n'.join(actions) + '\n' # Elasticsearch bulk data format requires newlines between actions + response = self.client.post( + f"/{self.index_name}/_bulk", + content=bulk_data, + headers={'Content-Type': 'application/x-ndjson'} + ) + if response.status_code != 200: + raise Exception(f"Failed to add documents: {response.text}") + + def get(self, session_id: str = None, agent_id: str = None, top_k=50, trace_id: str = None, **kwargs) -> List[ + ConversationMessage]: + """Get messages from the Elasticsearch index. + + Args: + session_id (str): The session id of the memory to get. + agent_id (str): The agent id of the memory to get. + top_k (int): The number of messages to get. + + Returns: + List[Message]: The list of messages retrieved from Elasticsearch. + """ + query = { + "query": { + "bool": { + "must": [] + } + }, + "size": top_k, + "sort": [ + {"timestamp": {"order": "desc"}} # Sorting by timestamp in ascending order + ] + } + if session_id: + query['query']['bool']['must'].append({"term": {"session_id": session_id}}) + if 'type' in kwargs: + if isinstance(kwargs['type'], list): + memory_types = kwargs['type'] + elif isinstance(kwargs['type'], str): + memory_types = [kwargs['type']] + else: + raise ValueError("type must be a list or a string") + query['query']['bool']['must'].append({"terms": {"type": memory_types}}) + if agent_id: + condition = { + "bool": { + "must": [ + {"match": {"target": agent_id}}, + {"match": {"target_type": 'agent'}} + ] + } + } + if kwargs.get('memory_types') and len(kwargs['memory_types']) > 0: + types_condition = { + "bool": { + "must": [ + {"match": {"source": agent_id}}, + {"match": {"source_type": 'agent'}}, + {"terms": {"target_type": kwargs['memory_types']}} + ] + } + } + query['query']['bool']['must'].append( + {"bool": {"should": [condition, types_condition]}}) + else: + query['query']['bool']['must'].append(condition) + + if trace_id: + query['query']['bool']['must'].append({"term": {"trace_id": trace_id}}) + response = self.client.post( + f'/{self.index_name}/_search', + json=query + ) + if response.status_code != 200: + raise Exception(f"Failed to retrieve documents: {response.text}") + + hits = response.json()['hits']['hits'] + messages = [] + for hit in hits: + messages.append(self.memory_converter.from_es_hit(hit)) + messages.reverse() + return messages + + def _client(self): + transport = httpx.HTTPTransport(retries=3) + if self.user and self.password: + return httpx.Client( + base_url=self.es_url, + transport=transport, + timeout=self.timeout, + auth=(self.user, self.password), + ) + return httpx.Client( + base_url=self.es_url, + transport=transport, + timeout=self.timeout, + ) + + +class DefaultMemoryConverter: + """The default memory converter for ElasticsearchMemoryStorage.""" + + def __init__(self, index_name: str, **kwargs: Any): + super().__init__(**kwargs) + self.index_name = index_name + + def from_es_hit(self, es_hit: dict) -> Message: + """Convert an Elasticsearch hit to a Message instance.""" + return ConversationMessage.from_dict({ + 'id': es_hit['_id'], + 'conversation_id': es_hit['_source']['session_id'], + 'source': es_hit['_source']['source'], + 'source_type': es_hit['_source']['source_type'], + 'target': es_hit['_source']['target'], + 'target_type': es_hit['_source']['target_type'], + 'content': es_hit['_source']['content'], + 'metadata': { + 'prefix': es_hit['_source'].get('prefix'), + 'timestamp': datetime.datetime.fromisoformat(es_hit['_source']['timestamp']), + 'params': es_hit['_source'].get('params'), + 'pair_id': es_hit['_source'].get('pair_id'), + 'gmt_created': datetime.datetime.fromisoformat(es_hit['_source']['timestamp']).isoformat(), + }, + 'type': es_hit['_source']['type'], + 'trace_id': es_hit['_source']['trace_id'], + 'additional_args': es_hit['_source'].get('additional_args') + }) + + def to_es_action(self, message: ConversationMessage, session_id: str = None, agent_id: str = None, **kwargs) -> str: + """Convert a message to an Elasticsearch action (index operation).""" + document = { + "session_id": session_id, + "content": message.content, + "trace_id": message.trace_id, + "source": message.source, + "source_type": message.source_type, + "target": message.target, + "target_type": message.target_type, + "type": message.type, + "prefix": message.metadata.get("prefix"), + "timestamp": message.metadata.get("timestamp", datetime.datetime.now()).isoformat(), + "params": message.metadata.get("params"), + "pair_id": message.metadata.get("pair_id"), + "additional_args": message.metadata.get("additional_args") + + } + index_info = { + "index": {"_index": self.index_name, "_id": message.id} + } + return f'{json.dumps(index_info)}\n{json.dumps(document)}' # Format for Elasticsearch bulk requests diff --git a/agentuniverse/agent/memory/conversation_memory/memory_storage/sqlite_conversation_memory_storage.py b/agentuniverse/agent/memory/conversation_memory/memory_storage/sqlite_conversation_memory_storage.py new file mode 100644 index 00000000..cb4aa337 --- /dev/null +++ b/agentuniverse/agent/memory/conversation_memory/memory_storage/sqlite_conversation_memory_storage.py @@ -0,0 +1,334 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/10/10 19:45 +# @Author : wangchongshi +# @Email : wangchongshi.wcs@antgroup.com +# @FileName: sql_alchemy_memory_storage.py +import datetime +import json +import uuid +from abc import abstractmethod +from typing import Optional, List, Any + +from pydantic import BaseModel, ConfigDict +from sqlalchemy.orm import declarative_base, sessionmaker, Session +from sqlalchemy import Integer, String, DateTime, Text, Column, Index, and_, func, or_, create_engine, Engine, insert + +from agentuniverse.agent.memory.conversation_memory.conversation_message import ConversationMessage +from agentuniverse.agent.memory.conversation_memory.enum import ConversationMessageEnum, ConversationMessageSourceType +from agentuniverse.agent.memory.memory_storage.memory_storage import MemoryStorage +from agentuniverse.agent.memory.message import Message +from agentuniverse.base.config.component_configer.component_configer import ComponentConfiger +from agentuniverse.database.sqldb_wrapper import SQLDBWrapper + + +class BaseMemoryConverter(BaseModel): + """ The base class for memory converter used for converting between aU Message and SQLAlchemy model. + + Attributes: + model_class: The SQLAlchemy model class. + model_config: The model configuration. + """ + + model_class: Any = None + model_config = ConfigDict(protected_namespaces=()) + + @abstractmethod + def from_sql_model(self, sql_message: Any) -> ConversationMessage: + """Convert a SQLAlchemy model to a Message instance.""" + raise NotImplementedError + + @abstractmethod + def to_sql_model(self, message: ConversationMessage, **kwargs) -> Any: + """Convert a Message instance to a SQLAlchemy model.""" + raise NotImplementedError + + @abstractmethod + def get_sql_model_class(self) -> Any: + """Get the SQLAlchemy model class.""" + raise NotImplementedError + + +def create_memory_model(table_name: str, DynamicBase: Any) -> Any: + """ + Create a memory model for a given table name. + + Args: + table_name: The name of the table to use. + DynamicBase: The base class to use for the model. + + Returns: + The model class. + """ + + class MemoryModel(DynamicBase): + """The default memory model for SqlAlchemyMemory.""" + __tablename__ = table_name + id = Column(Integer, primary_key=True, autoincrement=True) + session_id = Column(String(100), default='') + content = Column(Text) + trace_id = Column(String(100), default='') + source = Column(String(50), default='') + source_type = Column(String(50), default='') + target = Column(String(50), default='') + target_type = Column(String(50), default='') + type = Column(String(50), default='') + prefix = Column(String(200), default='') + timestamp = Column(DateTime, default=func.now()) + params = Column(Text) + pair_id = Column(String(50), default=0) + message_id = Column(String(100), unique=True) + additional_args = Column(Text) + + __table_args__ = ( + Index(f"idx_{table_name}_session_id_source", 'session_id', 'source', 'source_type'), + Index(f"idx_{table_name}_session_id_source_type", 'session_id', 'target', 'target_type'), + Index(f"idx_{table_name}_session_id_gmt_created", 'session_id', 'timestamp'), + Index(f"idx_{table_name}_message_id_unique", 'message_id', unique=True) + ) + + return MemoryModel + + +class DefaultMemoryConverter(BaseMemoryConverter): + """The default memory converter for SqlAlchemyMemory.""" + + def __init__(self, table_name: str, **kwargs: Any): + super().__init__(**kwargs) + self.model_class = create_memory_model(table_name, declarative_base()) + + def from_sql_model(self, sql_message: Any) -> Message: + """Convert a SQLAlchemy model to a Message instance.""" + return ConversationMessage.from_dict({'id': sql_message.message_id, + 'conversation_id': sql_message.session_id, + 'source': sql_message.source, + 'source_type': sql_message.source_type, + 'target': sql_message.target, + 'target_type': sql_message.target_type, + 'content': sql_message.content, + 'metadata': { + 'prefix': sql_message.prefix, + 'timestamp': sql_message.timestamp, + 'params': sql_message.params, + 'pair_id': sql_message.pair_id, + 'gmt_created': sql_message.timestamp + }, + 'type': sql_message.type, + 'trace_id': sql_message.trace_id, + 'additional_args': json.loads(sql_message.additional_args) + }) + + def to_sql_model(self, message: ConversationMessage, session_id: str = None, **kwargs) -> Any: + """Convert a Message instance to a SQLAlchemy model.""" + return self.model_class( + session_id=session_id, content=message.content, + trace_id=message.trace_id, + source=message.source, + source_type=message.source_type, + target=message.target, + target_type=message.target_type, + type=message.type, + prefix=message.metadata.get('prefix'), + timestamp=message.metadata.get('timestamp', datetime.datetime.now()), + params=message.metadata.get('params'), + pair_id=message.metadata.get('pair_id'), + message_id=message.id or uuid.uuid4().hex, + additional_args=json.dumps(message.additional_args) + ) + + def get_sql_model_class(self) -> Any: + """Get the SQLAlchemy model class.""" + return self.model_class + + +class SqliteMemoryStorage(MemoryStorage): + """SqlAlchemyMemoryStorage class that stores messages in a SQL database. + + Attributes: + sqldb_table_name (str): The name of the table to store for the memory. + sqldb_wrapper_name (str): The name of the SQLDBWrapper to use for the memory. + memory_converter (BaseMemoryConverter): The memory converter to use for the memory. + _sqldb_wrapper (SQLDBWrapper): The SQLDBWrapper instance to use for the memory. + """ + + sqldb_table_name: Optional[str] = 'memory' + sqldb_path: Optional[str] = None + memory_converter: BaseMemoryConverter = None + engine: Optional[Engine] = None + session: Optional[Any] = None + + model_config = { + "arbitrary_types_allowed": True, # 允许任意类型 + } + + def _new_client(self): + self.engine = create_engine(self.sqldb_path, echo=False) + self.session = sessionmaker(bind=self.engine) + self._init_db() + + def _initialize_by_component_configer(self, + memory_storage_config: ComponentConfiger) -> 'SqlAlchemyMemoryStorage': + """Initialize the SqlAlchemyMemoryStorage by the ComponentConfiger object. + + Args: + memory_storage_config(ComponentConfiger): A configer contains sql_alchemy_memory_storage basic info. + Returns: + SqlAlchemyMemoryStorage: A SqlAlchemyMemoryStorage instance. + """ + super()._initialize_by_component_configer(memory_storage_config) + if getattr(memory_storage_config, 'sql_table_name', None): + self.sqldb_table_name = memory_storage_config.sql_table_name + if getattr(memory_storage_config, 'sqldb_path', None): + self.sqldb_path = memory_storage_config.sqldb_path + if self.sqldb_path is None: + raise Exception('`sqldb_wrapper_name` is not set') + # initialize the memory converter if not set + if self.memory_converter is None: + self.memory_converter = DefaultMemoryConverter(self.sqldb_table_name) + self._new_client() + return self + + def _init_db(self) -> None: + self._create_table_if_not_exists() + + def _create_table_if_not_exists(self) -> None: + """Create the db table if it does not exist.""" + with self.engine.connect() as conn: + if not conn.dialect.has_table(conn, self.sqldb_table_name): + self.memory_converter.get_sql_model_class().__table__.create(conn) + + def delete(self, session_id: str = None, agent_id: str = None, trace_id: str = None, **kwargs) -> None: + """Delete the memory from the database. + + Args: + session_id (str): The session id of the memory to delete. + agent_id (str): The agent id of the memory to delete. + """ + if self.engine is None: + self._init_db() + if session_id is None and agent_id is None: + return + with self.session() as session: + model_class = self.memory_converter.get_sql_model_class() + query = session.query(model_class) + # construct query based on the provided session_id and agent_id + if session_id is not None: + query = query.filter(getattr(model_class, 'session_id') == session_id) + if agent_id is not None: + source_col = getattr(model_class, 'source') + type_col = getattr(model_class, 'type') + source_type_col = getattr(model_class, 'source_type') + source_condition = and_(source_col == agent_id, + type_col == ConversationMessageEnum.OUTPUT.value, + source_type_col == ConversationMessageSourceType.AGENT.value + ) + target_col = getattr(model_class, 'target') + target_type_col = getattr(model_class, 'target_type') + + target_condition = and_(target_col == agent_id, + type_col == ConversationMessageEnum.INPUT.value, + target_type_col == ConversationMessageSourceType.AGENT.value) + agent_id_col = or_(source_condition, target_condition) + + query.filter(agent_id_col) + if trace_id is not None: + query.filter(getattr(model_class, 'trace_id') == trace_id) + + # execute delete and commit the session + query.delete(synchronize_session=False) + session.commit() + + def add(self, message_list: List[ConversationMessage], session_id: str = None, agent_id: str = None, + **kwargs) -> None: + """Add messages to the memory db. + + Args: + message_list (List[Message]): The list of messages to add. + session_id (str): The session id of the memory to add. + agent_id (str): The agent id of the memory to add. + """ + message_list = ConversationMessage.check_and_convert_message(message_list, session_id) + if self.engine is None: + self._init_db() + if message_list is None: + return + + with self.session() as session: + for message in message_list: + existing_message = session.query(self.memory_converter.get_sql_model_class()).filter_by( + message_id=message.id).first() + if not existing_message: + session.add(self.memory_converter.to_sql_model(message=message, + session_id=session_id if session_id else None, + agent_id=agent_id, **kwargs)) + session.commit() + + def get(self, session_id: str = None, agent_id: str = None, top_k=20, trace_id: str = None, **kwargs) -> List[ + ConversationMessage]: + """Get messages from the memory db. + + Args: + session_id (str): The session id of the memory to get. + agent_id (str): The agent id of the memory to get. + top_k (int): The number of messages to get. + + Returns: + List[Message]: The list of messages retrieved from the memory. + """ + if self.session is None: + self._init_db() + with self.session() as session: + # get the messages from the memory by session_id and agent_id + model_class = self.memory_converter.get_sql_model_class() + conditions = [] + + # conditionally add session_id to the query + if session_id: + session_id_col = getattr(model_class, 'session_id') + conditions.append(session_id_col == session_id) + + source_col = getattr(model_class, 'source') + type_col = getattr(model_class, 'type') + agent_type_col = getattr(model_class, 'source_type') + target_col = getattr(model_class, 'target') + target_agent_type_col = getattr(model_class, 'target_type') + if 'type' in kwargs: + memory_type_col = getattr(model_class, 'type') + if isinstance(kwargs['type'], list): + conditions.append(memory_type_col.in_(kwargs['type'])) + elif isinstance(kwargs['type'], str): + conditions.append(type_col == kwargs['type']) + else: + raise ValueError("type must be a list or str") + if agent_id: + agent_qa_col = and_(target_col == agent_id, + target_agent_type_col == ConversationMessageSourceType.AGENT.value) + if kwargs.get("memory_types", None) and len(kwargs["memory_types"]) > 0: + types_col = and_(source_col == agent_id, + agent_type_col == ConversationMessageSourceType.AGENT.value, + target_agent_type_col.in_(kwargs["memory_types"])) + conditions.append(or_( + types_col, + agent_qa_col + )) + else: + conditions.append(agent_qa_col) + if trace_id: + trace_id_col = getattr(model_class, 'trace_id') + conditions.append(trace_id_col == trace_id) + # build the query with dynamic conditions + query = session.query(self.memory_converter.model_class) + if conditions: + query = query.where(and_(*conditions)) + query = query.order_by(model_class.timestamp.asc()) + + # Execute the query and fetch the results + records = query.all() + + records = records[-top_k:] + + messages = [] + for record in records: + messages.append(self.memory_converter.from_sql_model(record)) + return messages diff --git a/agentuniverse/agent/memory/enum.py b/agentuniverse/agent/memory/enum.py index 52a73b49..a74da09f 100644 --- a/agentuniverse/agent/memory/enum.py +++ b/agentuniverse/agent/memory/enum.py @@ -20,3 +20,5 @@ class ChatMessageEnum(Enum): SYSTEM = 'system' HUMAN = 'human' AI = 'ai' + INPUT = 'input' + OUTPUT = 'output' diff --git a/agentuniverse/agent/memory/memory.py b/agentuniverse/agent/memory/memory.py index d2681958..0a76c100 100644 --- a/agentuniverse/agent/memory/memory.py +++ b/agentuniverse/agent/memory/memory.py @@ -1,6 +1,6 @@ # !/usr/bin/env python3 # -*- coding:utf-8 -*- - +import datetime # @Time : 2024/3/15 10:05 # @Author : wangchongshi # @Email : wangchongshi.wcs@antgroup.com @@ -9,17 +9,19 @@ from langchain_core.memory import BaseMemory from pydantic import Extra +from agentuniverse.agent.agent_manager import AgentManager from agentuniverse.agent.memory.enum import MemoryTypeEnum from agentuniverse.agent.memory.memory_compressor.memory_compressor import MemoryCompressor from agentuniverse.agent.memory.memory_compressor.memory_compressor_manager import MemoryCompressorManager from agentuniverse.agent.memory.memory_storage.memory_storage import MemoryStorage from agentuniverse.agent.memory.memory_storage.memory_storage_manager import MemoryStorageManager from agentuniverse.agent.memory.message import Message +from agentuniverse.agent.output_object import OutputObject from agentuniverse.base.component.component_base import ComponentBase from agentuniverse.base.component.component_enum import ComponentEnum from agentuniverse.base.config.application_configer.application_config_manager import ApplicationConfigManager from agentuniverse.base.config.component_configer.configers.memory_configer import MemoryConfiger -from agentuniverse.base.util.memory_util import get_memory_tokens +from agentuniverse.base.util.memory_util import get_memory_tokens, get_memory_string class Memory(ComponentBase): @@ -44,6 +46,7 @@ class Memory(ComponentBase): memory_compressor: Optional[str] = None memory_storages: Optional[List[str]] = ['local_memory_storage'] memory_retrieval_storage: Optional[str] = None + summarize_agent_id: Optional[str] = 'memory_summarize_agent' class Config: extra = Extra.allow @@ -72,12 +75,22 @@ def delete(self, session_id: str = None, **kwargs) -> None: if memory_storage: memory_storage.delete(session_id, **kwargs) - def get(self, session_id: str = None, agent_id: str = None, **kwargs) -> List[Message]: + def get(self, session_id: str = None, agent_id: str = None, prune: bool = False, **kwargs) -> List[Message]: + """Get messages from the memory.""" + memory_storage: MemoryStorage = MemoryStorageManager().get_instance_obj(self.memory_retrieval_storage) + if memory_storage: + memories = memory_storage.get(session_id, agent_id, **kwargs) + if prune: + memories = self.prune(memories) + return memories + return [] + + def get_with_no_prune(self, session_id: str = None, agent_id: str = None, **kwargs) -> List[Message]: """Get messages from the memory.""" memory_storage: MemoryStorage = MemoryStorageManager().get_instance_obj(self.memory_retrieval_storage) if memory_storage: memories = memory_storage.get(session_id, agent_id, **kwargs) - return self.prune(memories) + return memories return [] def prune(self, memories: List[Message]) -> List[Message]: @@ -117,6 +130,17 @@ def set_by_agent_model(self, **kwargs): copied_obj.agent_llm_name = kwargs['agent_llm_name'] return copied_obj + def summarize_memory(self, **kwargs) -> str: + kwargs['prune'] = False + messages = self.get(**kwargs) + summarize_messages = self.get(session_id=kwargs.get('session_id'), agent_id=kwargs.get('agent_id'), + type='summarize') + summarize_content = summarize_messages[-1].content if summarize_messages and len(summarize_messages) > 0 else '' + messages_str = get_memory_string(messages) + agent: 'Agent' = AgentManager().get_instance_obj(self.summarize_agent_id) + output_object: OutputObject = agent.run(input=messages_str, summarize_content=summarize_content) + return output_object.get_data('output') + def get_instance_code(self) -> str: """Return the full name of the memory.""" appname = ApplicationConfigManager().app_configer.base_info_appname @@ -147,4 +171,6 @@ def initialize_by_component_configer(self, component_configer: MemoryConfiger) - self.memory_retrieval_storage = component_configer.memory_retrieval_storage if not self.memory_retrieval_storage: self.memory_retrieval_storage = self.memory_storages[0] + if component_configer.memory_summarize_agent: + self.summarize_agent_id = component_configer.memory_summarize_agent return self diff --git a/agentuniverse/agent/memory/memory_compressor/memory_compressor.py b/agentuniverse/agent/memory/memory_compressor/memory_compressor.py index 4391fa13..c48e9674 100644 --- a/agentuniverse/agent/memory/memory_compressor/memory_compressor.py +++ b/agentuniverse/agent/memory/memory_compressor/memory_compressor.py @@ -53,8 +53,9 @@ def compress_memory(self, new_memories: List[Message], max_tokens: int = 500, ex if prompt and llm: new_memory_str = get_memory_string(new_memories) chain = prompt.as_langchain() | llm.as_langchain() | StrOutputParser() - return chain.invoke( + result = chain.invoke( input={'new_lines': new_memory_str, 'summary': existing_memory, 'max_tokens': max_tokens}) + return result else: return '' diff --git a/agentuniverse/agent/memory/memory_storage/chroma_memory_storage.py b/agentuniverse/agent/memory/memory_storage/chroma_memory_storage.py index d01f643a..9642d0b6 100644 --- a/agentuniverse/agent/memory/memory_storage/chroma_memory_storage.py +++ b/agentuniverse/agent/memory/memory_storage/chroma_memory_storage.py @@ -122,7 +122,8 @@ def add(self, message_list: List[Message], session_id: str = None, agent_id: str embeddings=[embedding] if len(embedding) > 0 else None, ) - def get(self, session_id: str = None, agent_id: str = None, top_k=10, input: str = '', source: str = None, **kwargs) -> \ + def get(self, session_id: str = None, agent_id: str = None, top_k=10, input: str = '', source: str = None, + **kwargs) -> \ List[Message]: """Get messages from the memory db. @@ -144,6 +145,12 @@ def get(self, session_id: str = None, agent_id: str = None, top_k=10, input: str filters["$and"].append({'agent_id': agent_id}) if source: filters["$and"].append({'source': source}) + if kwargs.get('type'): + if isinstance(kwargs.get('type'), list): + types = kwargs.get('type') + elif isinstance(kwargs.get('type'), str): + types = [kwargs.get('type')] + filters["$and"].append({'type': {'$in': types}}) if len(filters["$and"]) < 2: filters = filters["$and"][0] if len(filters["$and"]) == 1 else {} if input: @@ -164,6 +171,7 @@ def get(self, session_id: str = None, agent_id: str = None, top_k=10, input: str else: results = self._collection.get(where=filters) messages = self.to_messages(result=results, sort_by_time=True) + messages.reverse() return messages[-top_k:] def to_messages(self, result: dict, sort_by_time: bool = False) -> List[Message]: diff --git a/agentuniverse/agent/memory/memory_storage/sql_alchemy_memory_storage.py b/agentuniverse/agent/memory/memory_storage/sql_alchemy_memory_storage.py index 82c58463..18d9dd48 100644 --- a/agentuniverse/agent/memory/memory_storage/sql_alchemy_memory_storage.py +++ b/agentuniverse/agent/memory/memory_storage/sql_alchemy_memory_storage.py @@ -229,6 +229,13 @@ def get(self, session_id: str = None, agent_id: str = None, top_k=10, source: st if source: source_col = getattr(model_class, 'source') conditions.append(source_col == source) + if kwargs.get('type'): + if isinstance(kwargs['type'], list): + types = kwargs['type'] + if not isinstance(kwargs['type'], str): + types = [kwargs['type']] + type_col = getattr(model_class, 'type') + conditions.append(conditions.append(type_col.in_(types))) # build the query with dynamic conditions query = session.query(self.memory_converter.model_class) diff --git a/agentuniverse/agent/plan/planner/react_planner/react_planner.py b/agentuniverse/agent/plan/planner/react_planner/react_planner.py index b7fd9ca8..20c6e869 100644 --- a/agentuniverse/agent/plan/planner/react_planner/react_planner.py +++ b/agentuniverse/agent/plan/planner/react_planner/react_planner.py @@ -27,7 +27,8 @@ from agentuniverse.agent.input_object import InputObject from agentuniverse.agent.memory.memory import Memory from agentuniverse.agent.plan.planner.planner import Planner -from agentuniverse.agent.plan.planner.react_planner.stream_callback import StreamOutPutCallbackHandler +from agentuniverse.agent.plan.planner.react_planner.stream_callback import StreamOutPutCallbackHandler, \ + InvokeCallbackHandler from agentuniverse.base.util.agent_util import assemble_memory_input from agentuniverse.base.util.prompt_util import process_llm_token from agentuniverse.llm.llm import LLM @@ -81,6 +82,8 @@ def get_run_config(agent_model: AgentModel, input_object: InputObject) -> Runnab callbacks = [] output_stream = input_object.get_data('output_stream') callbacks.append(StreamOutPutCallbackHandler(output_stream, agent_info=agent_model.info)) + callbacks.append(InvokeCallbackHandler(source=agent_model.info.get('name'), + llm_name=agent_model.profile.get('llm_model').get('name'))) config.setdefault("callbacks", callbacks) return config diff --git a/agentuniverse/agent/plan/planner/react_planner/stream_callback.py b/agentuniverse/agent/plan/planner/react_planner/stream_callback.py index f6c4cfca..020be07b 100644 --- a/agentuniverse/agent/plan/planner/react_planner/stream_callback.py +++ b/agentuniverse/agent/plan/planner/react_planner/stream_callback.py @@ -7,18 +7,21 @@ # @FileName: stream_callback.py import asyncio -from typing import Optional, Dict, Any, Union +from typing import Optional, Dict, Any, Union, List from uuid import UUID from langchain_core.agents import AgentAction, AgentFinish from langchain_core.callbacks import BaseCallbackHandler -from langchain_core.outputs import GenerationChunk, ChatGenerationChunk +from langchain_core.outputs import GenerationChunk, ChatGenerationChunk, LLMResult + +from agentuniverse.agent.memory.conversation_memory.conversation_memory_module import ConversationMemoryModule class StreamOutPutCallbackHandler(BaseCallbackHandler): """Callback Handler that prints to std out.""" - def __init__(self, queue_stream: asyncio.Queue, color: Optional[str] = None, agent_info: dict = None) -> None: + def __init__(self, queue_stream: asyncio.Queue, color: Optional[str] = None, agent_info: dict = None, + **kwargs) -> None: """Initialize callback handler.""" self.queueStream = queue_stream self.color = color @@ -45,6 +48,30 @@ def on_agent_action( } }) + def on_tool_start( + self, + serialized: Dict[str, Any], + input_str: str, + *, + run_id: UUID, + parent_run_id: Optional[UUID] = None, + tags: Optional[List[str]] = None, + metadata: Optional[Dict[str, Any]] = None, + inputs: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ) -> Any: + ConversationMemoryModule().add_tool_input_info( + start_info={ + "source": self.agent_info.get('name'), + "type": 'agent', + }, + target=serialized.get('name'), + params={ + "input": input_str + }, + pair_id=f"tool_{run_id.hex}" + ) + def on_llm_new_token( self, token: str, @@ -88,6 +115,17 @@ def on_tool_end( "agent_info": self.agent_info } }) + ConversationMemoryModule().add_tool_output_info( + start_info={ + "source": self.agent_info.get('name'), + "type": 'agent', + }, + target=kwargs.get('name'), + params={ + "output": output + }, + pair_id=f"tool_{kwargs.get('run_id').hex}" + ) def on_text( self, @@ -109,3 +147,52 @@ def on_agent_finish( "agent_info": self.agent_info } }) + + +class InvokeCallbackHandler(BaseCallbackHandler): + """Callback Handler that prints to std out.""" + source: str + llm_name: str + + def __init__(self, source: str, llm_name: str) -> None: + """Initialize callback handler.""" + self.source = source + self.llm_name = llm_name + + def on_llm_start( + self, + serialized: Dict[str, Any], + prompts: List[str], + *, + run_id: UUID, + parent_run_id: Optional[UUID] = None, + tags: Optional[List[str]] = None, + metadata: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ) -> Any: + prompt = "\n".join(prompts) + + start_info = { + "source": self.source, + "type": "agent", + } + + ConversationMemoryModule().add_llm_input_info(start_info, self.llm_name, prompt, f"llm_{run_id.hex}") + + def on_llm_end( + self, + response: LLMResult, + *, + run_id: UUID, + parent_run_id: Optional[UUID] = None, + **kwargs: Any, + ) -> Any: + start_info = { + "source": self.source, + "type": "agent", + } + ConversationMemoryModule().add_llm_output_info( + start_info, self.llm_name, + response.generations[0][0].text, + f"llm_{run_id.hex}" + ) diff --git a/agentuniverse/agent/template/agent_template.py b/agentuniverse/agent/template/agent_template.py index 4f701629..a7f31fa0 100644 --- a/agentuniverse/agent/template/agent_template.py +++ b/agentuniverse/agent/template/agent_template.py @@ -28,6 +28,7 @@ class AgentTemplate(Agent, ABC): tool_names: Optional[list[str]] = None knowledge_names: Optional[list[str]] = None prompt_version: Optional[str] = None + conversation_memory_name: Optional[str] = None def execute(self, input_object: InputObject, agent_input: dict, **kwargs) -> dict: memory: Memory = self.process_memory(agent_input, **kwargs) @@ -43,27 +44,26 @@ async def async_execute(self, input_object: InputObject, agent_input: dict, **kw def customized_execute(self, input_object: InputObject, agent_input: dict, memory: Memory, llm: LLM, prompt: Prompt, **kwargs) -> dict: - assemble_memory_input(memory, agent_input) + self.load_memory(memory, agent_input) process_llm_token(llm, prompt.as_langchain(), self.agent_model.profile, agent_input) chain = prompt.as_langchain() | llm.as_langchain_runnable( self.agent_model.llm_params()) | StrOutputParser() res = self.invoke_chain(chain, agent_input, input_object, **kwargs) - assemble_memory_output(memory=memory, - agent_input=agent_input, - content=f"Human: {agent_input.get('input')}, AI: {res}") + self.add_memory(memory, f"Human: {agent_input.get('input')}, AI: {res}", agent_input=agent_input) self.add_output_stream(input_object.get_data('output_stream'), res) return {**agent_input, 'output': res} async def customized_async_execute(self, input_object: InputObject, agent_input: dict, memory: Memory, llm: LLM, prompt: Prompt, **kwargs) -> dict: - assemble_memory_input(memory, agent_input) + assemble_memory_input(memory, agent_input, self.get_memory_params(agent_input)) process_llm_token(llm, prompt.as_langchain(), self.agent_model.profile, agent_input) chain = prompt.as_langchain() | llm.as_langchain_runnable( self.agent_model.llm_params()) | StrOutputParser() res = await self.async_invoke_chain(chain, agent_input, input_object, **kwargs) - assemble_memory_output(memory=memory, - agent_input=agent_input, - content=f"Human: {agent_input.get('input')}, AI: {res}") + if self.memory_name: + assemble_memory_output(memory=memory, + agent_input=agent_input, + content=f"Human: {agent_input.get('input')}, AI: {res}") self.add_output_stream(input_object.get_data('output_stream'), res) return {**agent_input, 'output': res} @@ -79,6 +79,7 @@ def initialize_by_component_configer(self, component_configer: AgentConfiger) -> self.memory_name = self.agent_model.memory.get('name') self.tool_names = self.agent_model.action.get('tool', []) self.knowledge_names = self.agent_model.action.get('knowledge', []) + self.conversation_memory_name = self.agent_model.memory.get('conversation_memory', '') return self def process_llm(self, **kwargs) -> LLM: diff --git a/agentuniverse/agent/template/default_summarize_agent_template.py b/agentuniverse/agent/template/default_summarize_agent_template.py new file mode 100644 index 00000000..8a7f11b0 --- /dev/null +++ b/agentuniverse/agent/template/default_summarize_agent_template.py @@ -0,0 +1,26 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/10/24 21:19 +# @Author : wangchongshi +# @Email : wangchongshi.wcs@antgroup.com +# @FileName: rag_template.py +from agentuniverse.agent.input_object import InputObject +from agentuniverse.agent.template.rag_agent_template import RagAgentTemplate + + +class SummarizeRagAgentTemplate(RagAgentTemplate): + + def input_keys(self) -> list[str]: + return ['input', 'summarize_content'] + + def output_keys(self) -> list[str]: + return ['output'] + + def parse_input(self, input_object: InputObject, agent_input: dict) -> dict: + agent_input['input'] = input_object.get_data('input') + agent_input['summarize_content'] = input_object.get_data('summarize_content') + return agent_input + + def parse_result(self, agent_result: dict) -> dict: + return {**agent_result, 'output': agent_result['output']} diff --git a/agentuniverse/agent/template/executing_agent_template.py b/agentuniverse/agent/template/executing_agent_template.py index 212efbcc..81177b6c 100644 --- a/agentuniverse/agent/template/executing_agent_template.py +++ b/agentuniverse/agent/template/executing_agent_template.py @@ -6,6 +6,7 @@ # @Email : wangchongshi.wcs@antgroup.com # @FileName: executing_agent_template.py import asyncio +import uuid from concurrent.futures import ThreadPoolExecutor, as_completed import time from typing import Optional @@ -14,7 +15,9 @@ from agentuniverse.agent.action.tool.tool_manager import ToolManager from agentuniverse.agent.input_object import InputObject +from agentuniverse.agent.memory.conversation_memory.conversation_memory_module import ConversationMemoryModule from agentuniverse.agent.memory.memory import Memory +from agentuniverse.agent.output_object import OutputObject from agentuniverse.agent.template.agent_template import AgentTemplate from agentuniverse.base.config.component_configer.configers.agent_configer import AgentConfiger from agentuniverse.base.context.framework_context_manager import FrameworkContextManager @@ -57,15 +60,19 @@ async def customized_async_execute(self, input_object: InputObject, agent_input: def _execute_tasks(self, input_object: InputObject, agent_input: dict, memory: Memory, llm: LLM, prompt: Prompt, **kwargs) -> dict: self._context_values: dict = FrameworkContextManager().get_all_contexts() - + _context_values: dict = FrameworkContextManager().get_all_contexts() framework = agent_input.get('framework', []) + if len(framework) == 0: + return {'executing_result': [], + 'output_stream': input_object.get_data('output_stream', None)} + with ThreadPoolExecutor(max_workers=min(len(framework), 10), thread_name_prefix="executing_agent_template") as thread_executor: futures = [] for i, subtask in enumerate(framework): future = thread_executor.submit(self._execute_subtask, subtask, input_object, agent_input, i, memory, - llm, prompt) + llm, prompt, context_values=_context_values) futures.append(future) time.sleep(1) @@ -75,14 +82,18 @@ def _execute_tasks(self, input_object: InputObject, agent_input: dict, memory: M return {'executing_result': [result for result in executing_result], 'output_stream': input_object.get_data('output_stream', None)} - def _execute_subtask(self, subtask, input_object, agent_input, index, memory, llm, prompt) -> dict: + def _execute_subtask(self, subtask, input_object, agent_input, index, memory, llm, prompt, **kwargs) -> dict: context_tokens = {} + FrameworkContextManager().set_all_contexts(kwargs.get('context_values', {})) try: - # pass the framework context into the thread. - for var_name, var_value in self._context_values.items(): - token = FrameworkContextManager().set_context(var_name, var_value) - context_tokens[var_name] = token - + pair_id = uuid.uuid4().hex + ConversationMemoryModule().add_agent_input_info( + start_info=input_object.get_data('memory_source_info'), + instance=self, + params={'input': agent_input.get('framework')[index]}, + pair_id=pair_id, + auto=False + ) input_object_copy = InputObject(input_object.to_dict()) agent_input_copy = dict(agent_input) @@ -94,15 +105,18 @@ def _execute_subtask(self, subtask, input_object, agent_input, index, memory, ll agent_input_copy['input'] = subtask process_llm_token(llm, prompt.as_langchain(), self.agent_model.profile, agent_input_copy) - assemble_memory_input(memory, agent_input_copy) - + self.load_memory(memory, agent_input_copy) chain = prompt.as_langchain() | llm.as_langchain_runnable( self.agent_model.llm_params()) | StrOutputParser() res = self.invoke_chain(chain, agent_input_copy, input_object_copy) - - assemble_memory_output(memory=memory, - agent_input=agent_input, - content=f"Human: {agent_input.get('input')}, AI: {res}") + self.add_memory(memory, f"Human: {agent_input.get('input')}, AI: {res}", agent_input=agent_input) + ConversationMemoryModule().add_agent_result_info( + agent_instance=self, + agent_result={'output': res}, + target_info=input_object.get_data('memory_source_info'), + pair_id=pair_id, + auto=False + ) return { 'index': index, 'input': f"Question {index + 1}: {subtask}", diff --git a/agentuniverse/agent/template/react_agent_template.py b/agentuniverse/agent/template/react_agent_template.py index a563958a..a3e1f067 100644 --- a/agentuniverse/agent/template/react_agent_template.py +++ b/agentuniverse/agent/template/react_agent_template.py @@ -27,7 +27,8 @@ from agentuniverse.agent.agent_manager import AgentManager from agentuniverse.agent.input_object import InputObject from agentuniverse.agent.memory.memory import Memory -from agentuniverse.agent.plan.planner.react_planner.stream_callback import StreamOutPutCallbackHandler +from agentuniverse.agent.plan.planner.react_planner.stream_callback import StreamOutPutCallbackHandler, \ + InvokeCallbackHandler from agentuniverse.base.util.prompt_util import process_llm_token from agentuniverse.llm.llm import LLM from agentuniverse.prompt.prompt import Prompt @@ -57,7 +58,7 @@ def parse_result(self, agent_result: dict) -> dict: def customized_execute(self, input_object: InputObject, agent_input: dict, memory: Memory, llm: LLM, prompt: Prompt, **kwargs) -> dict: - assemble_memory_input(memory, agent_input) + self.load_memory(memory, agent_input) process_llm_token(llm, prompt.as_langchain(), self.agent_model.profile, agent_input) lc_tools: List[LangchainTool] = self._convert_to_langchain_tool() agent = self.create_react_agent(llm.as_langchain(), lc_tools, prompt.as_langchain(), @@ -77,7 +78,7 @@ def customized_execute(self, input_object: InputObject, agent_input: dict, memor async def customized_async_execute(self, input_object: InputObject, agent_input: dict, memory: Memory, llm: LLM, prompt: Prompt, **kwargs) -> dict: - assemble_memory_input(memory, agent_input) + self.load_memory(memory, agent_input) process_llm_token(llm, prompt.as_langchain(), self.agent_model.profile, agent_input) lc_tools: List[LangchainTool] = self._convert_to_langchain_tool() agent = self.create_react_agent(llm.as_langchain(), lc_tools, prompt.as_langchain(), @@ -90,9 +91,8 @@ async def customized_async_execute(self, input_object: InputObject, agent_input: res = await agent_executor.ainvoke(input=agent_input, memory=memory.as_langchain() if memory else None, chat_history=agent_input.get(memory.memory_key) if memory else '', config=self._get_run_config(input_object)) - assemble_memory_output(memory=memory, - agent_input=agent_input, - content=f"Human: {agent_input.get('input')}, AI: {res.get('output')}") + self.add_memory(memory, content=f"Human: {agent_input.get('input')}, AI: {res.get('output')}", + agent_input=agent_input) return res def create_react_agent( @@ -163,6 +163,8 @@ def _get_run_config(self, input_object: InputObject) -> RunnableConfig: callbacks = [] output_stream = input_object.get_data('output_stream') callbacks.append(StreamOutPutCallbackHandler(output_stream, agent_info=self.agent_model.info)) + callbacks.append(InvokeCallbackHandler(source=self.agent_model.info.get('name'), + llm_name=self.agent_model.profile.get('llm_model').get('name'))) config.setdefault("callbacks", callbacks) return config diff --git a/agentuniverse/agent_serve/web/flask_server.py b/agentuniverse/agent_serve/web/flask_server.py index 9f6a1f84..9a9ecd83 100644 --- a/agentuniverse/agent_serve/web/flask_server.py +++ b/agentuniverse/agent_serve/web/flask_server.py @@ -1,12 +1,58 @@ import traceback - -from flask import Flask, Response +import time +from flask import Flask, Response, g, request from werkzeug.exceptions import HTTPException +from loguru import logger +from concurrent.futures import TimeoutError from ..service_instance import ServiceInstance, ServiceNotFoundError from .request_task import RequestTask -from .web_util import request_param, service_run_queue, make_standard_response +from .web_util import request_param, service_run_queue, make_standard_response, FlaskServerManager +from .thread_with_result import ThreadPoolExecutorWithReturnValue from ...base.util.logging.logging_util import LOGGER +from agentuniverse.base.util.logging.log_type_enum import LogTypeEnum +from agentuniverse.base.util.logging.general_logger import _get_context_prefix + +from werkzeug.local import LocalProxy + + +# Patch original flask request so it can be dumped by loguru. +class SerializableRequest: + def __init__(self, method, path, args, form, headers): + self.method = method + self.path = path + self.args = args + self.form = form + self.headers = headers + + def __repr__(self): + return f"" + + +def localproxy_reduce_ex(self, protocol): + real_obj = self._get_current_object() + return ( + SerializableRequest, + (real_obj.method, real_obj.path, dict(real_obj.args), dict(real_obj.form), dict(real_obj.headers)), + ) + + +LocalProxy.__reduce_ex__ = localproxy_reduce_ex + + +# log stream response +def timed_generator(generator, start_time): + try: + for data in generator: + yield data + finally: + elapsed_time = time.time() - start_time + logger.bind( + log_type=LogTypeEnum.flask_response, + flask_response="Stream finished", + elapsed_time=elapsed_time, + context_prefix=_get_context_prefix() + ).info("Stream finished.") app = Flask(__name__) @@ -14,6 +60,28 @@ app.json.ensure_ascii = False +@app.before_request +def before(): + logger.bind( + log_type=LogTypeEnum.flask_request, + flask_request=request, + context_prefix=_get_context_prefix() + ).info("Before request.") + g.start_time = time.time() + + +@app.after_request +def after_request(response): + if not response.mimetype == "text/event-stream": + logger.bind( + log_type=LogTypeEnum.flask_response, + flask_response=response, + elapsed_time=time.time() - g.start_time, + context_prefix=_get_context_prefix() + ).info("After request.") + return response + + @app.route("/echo") def echo(): return 'Welcome to agentUniverse!!!' @@ -42,9 +110,18 @@ def service_run(service_id: str, params: dict, saved: bool = False): result: This key points to a nested dictionary that includes the result of the task. """ - params = {} if params is None else params - request_task = RequestTask(ServiceInstance(service_id).run, saved, **params) - result = request_task.run() + try: + params = {} if params is None else params + request_task = RequestTask(ServiceInstance(service_id).run, saved, + **params) + with ThreadPoolExecutorWithReturnValue() as executor: + future = executor.submit(request_task.run) + result = future.result(timeout=FlaskServerManager().sync_service_timeout) + except TimeoutError: + return make_standard_response(success=False, + message="AU sync service timeout", + status_code=504) + return make_standard_response(success=True, result=result, request_id=request_task.request_id) @@ -65,7 +142,7 @@ def service_run_stream(service_id: str, params: dict, saved: bool = False): params = {} if params is None else params params['service_id'] = service_id task = RequestTask(service_run_queue, saved, **params) - response = Response(task.stream_run(), mimetype="text/event-stream") + response = Response(timed_generator(task.stream_run(),g.start_time), mimetype="text/event-stream") response.headers['X-Request-ID'] = task.request_id return response diff --git a/agentuniverse/agent_serve/web/request_task.py b/agentuniverse/agent_serve/web/request_task.py index 05a2c85b..53251fae 100644 --- a/agentuniverse/agent_serve/web/request_task.py +++ b/agentuniverse/agent_serve/web/request_task.py @@ -7,6 +7,7 @@ # @FileName: request_task.py import asyncio import enum +import traceback from enum import Enum import json import queue @@ -55,13 +56,13 @@ def __init__(self, func, saved=True, **kwargs): self.func: callable = func self.kwargs = kwargs self.request_id = uuid.uuid4().hex - self.queue = queue.Queue(maxsize=100) + self.queue = queue.Queue(maxsize=1000) self.thread: Optional[ThreadWithReturnValue] = None self.state = TaskStateEnum.INIT.value # Whether save to Database. self.saved = saved self.__request_do__ = self.add_request_do() - self.async_queue = asyncio.Queue(maxsize=200) + self.async_queue = asyncio.Queue(maxsize=2000) self.async_task = None def receive_steps(self): @@ -83,7 +84,7 @@ def receive_steps(self): yield "data:" + json.dumps({"result": result}, ensure_ascii=False) + "\n\n " except Exception as e: - LOGGER.error("request task execute Fail: " + str(e)) + LOGGER.error("request task execute Fail: " + str(e)+traceback.format_exc()) yield "data:" + json.dumps({"error": {"error_msg": str(e)}}) + "\n\n " async def async_receive_steps(self) -> AsyncIterator[str]: @@ -227,8 +228,8 @@ def add_request_do(self): result=dict(), steps=[], additional_args=dict(), - gmt_create=int(time.time()), - gmt_modified=int(time.time()), + gmt_create=datetime.now(), + gmt_modified=datetime.now(), ) if self.saved: RequestLibrary().add_request(request_do) diff --git a/agentuniverse/agent_serve/web/thread_with_result.py b/agentuniverse/agent_serve/web/thread_with_result.py index d2d9c86c..1f98c896 100644 --- a/agentuniverse/agent_serve/web/thread_with_result.py +++ b/agentuniverse/agent_serve/web/thread_with_result.py @@ -60,6 +60,7 @@ def result(self): class ThreadPoolExecutorWithReturnValue(ThreadPoolExecutor): + def _adjust_thread_count(self): # if idle threads are available, don't spin new threads if self._idle_semaphore.acquire(timeout=0): diff --git a/agentuniverse/agent_serve/web/web_util.py b/agentuniverse/agent_serve/web/web_util.py index 4844b698..c110f735 100644 --- a/agentuniverse/agent_serve/web/web_util.py +++ b/agentuniverse/agent_serve/web/web_util.py @@ -15,6 +15,21 @@ from ..service_instance import ServiceInstance from ...agent.agent import Agent from ...agent.agent_manager import AgentManager +from ...base.util.logging.logging_util import LOGGER +from ...base.annotation.singleton import singleton + + +@singleton +class FlaskServerManager: + _sync_service_timeout = 30 + + @property + def sync_service_timeout(self): + return self._sync_service_timeout + + @sync_service_timeout.setter + def sync_service_timeout(self, timeout): + self._sync_service_timeout = timeout def request_param(func): @@ -108,4 +123,5 @@ def make_standard_response(success: bool, "message": message, "request_id": request_id } + LOGGER.info(f"AU_FLASK_RESPONSE: {response_data}") return make_response(jsonify(response_data), status_code) diff --git a/agentuniverse/base/agentuniverse.py b/agentuniverse/base/agentuniverse.py index 796ed6bb..f976a1f1 100644 --- a/agentuniverse/base/agentuniverse.py +++ b/agentuniverse/base/agentuniverse.py @@ -28,6 +28,7 @@ from agentuniverse.agent_serve.web.rpc.grpc.grpc_server_booster import set_grpc_config from agentuniverse.agent_serve.web.web_booster import ACTIVATE_OPTIONS from agentuniverse.agent_serve.web.post_fork_queue import POST_FORK_QUEUE +from agentuniverse.agent_serve.web.web_util import FlaskServerManager @singleton @@ -51,6 +52,7 @@ def __init__(self): self.__system_default_memory_compressor_package = ['agentuniverse.agent.memory.memory_compressor'] self.__system_default_memory_storage_package = ['agentuniverse.agent.memory.memory_storage'] self.__system_default_work_pattern_package = ['agentuniverse.agent.work_pattern'] + self.__system_default_log_sink_package = ['agentuniverse.base.util.logging.log_sink.log_sink'] def start(self, config_path: str = None, core_mode: bool = False): """Start the agentUniverse framework. @@ -92,6 +94,9 @@ def start(self, config_path: str = None, core_mode: bool = False): set_grpc_config(configer) # Init gunicorn web server with config file. + sync_service_timeout = configer.value.get('HTTP_SERVER', {}).get('sync_service_timeout') + if sync_service_timeout: + FlaskServerManager().sync_service_timeout = sync_service_timeout gunicorn_activate = configer.value.get('GUNICORN', {}).get('activate') if gunicorn_activate and gunicorn_activate.lower() == 'true': ACTIVATE_OPTIONS["gunicorn"] = True @@ -157,6 +162,8 @@ def __scan_and_register(self, app_configer: AppConfiger): + self.__system_default_memory_storage_package) core_work_pattern_package_list = ((app_configer.core_work_pattern_package_list or app_configer.core_default_package_list) + self.__system_default_work_pattern_package) + core_log_sink_package_list = ((app_configer.core_log_sink_package_list or app_configer.core_default_package_list) + + self.__system_default_log_sink_package) component_package_map = { ComponentEnum.AGENT: core_agent_package_list, @@ -177,7 +184,8 @@ def __scan_and_register(self, app_configer: AppConfiger): ComponentEnum.QUERY_PARAPHRASER: core_query_paraphraser_package_list, ComponentEnum.MEMORY_COMPRESSOR: core_memory_compressor_package_list, ComponentEnum.MEMORY_STORAGE: core_memory_storage_package_list, - ComponentEnum.WORK_PATTERN: core_work_pattern_package_list + ComponentEnum.WORK_PATTERN: core_work_pattern_package_list, + ComponentEnum.LOG_SINK: core_log_sink_package_list } component_configer_list_map = {} diff --git a/agentuniverse/base/annotation/trace.py b/agentuniverse/base/annotation/trace.py index ec89755d..e083f570 100644 --- a/agentuniverse/base/annotation/trace.py +++ b/agentuniverse/base/annotation/trace.py @@ -8,12 +8,45 @@ import asyncio import functools import inspect +import time +import sys +import uuid from functools import wraps +from agentuniverse.agent.memory.conversation_memory.conversation_memory_module import ConversationMemoryModule +from agentuniverse.base.component.component_enum import ComponentEnum from agentuniverse.base.context.framework_context_manager import FrameworkContextManager from agentuniverse.base.util.monitor.monitor import Monitor from agentuniverse.llm.llm_output import LLMOutput +from agentuniverse.base.util.logging.logging_util import LOGGER + + +def _get_invocation_chain_str() -> str: + invocation_chain_str = '' + invocation_chain = Monitor.get_invocation_chain() + if len(invocation_chain) > 0: + invocation_chain_str = ' -> '.join( + [f"source: {d['source']}, type: {d['type']}" for + d in invocation_chain] + ) + invocation_chain_str += ' | ' + + return invocation_chain_str + + +def _get_au_trace_id_str() -> str: + au_trace_id_str = '' + trace_id = Monitor.get_trace_id() + if trace_id: + au_trace_id_str = f'aU trace id: {trace_id} | ' + return au_trace_id_str + + +def log_trace(log_detail: str): + au_trace_id_str = _get_au_trace_id_str() + invocation_chain_str = _get_invocation_chain_str() + LOGGER.info(au_trace_id_str + invocation_chain_str + log_detail) def trace_llm(func): @@ -22,6 +55,14 @@ def trace_llm(func): Decorator to trace the LLM invocation, add llm input and output to the monitor. """ + def log_llm_trace_output(output, start_time): + cost_time = time.time() - start_time + trace_log_str = f"LLM output is:{output}, cost {cost_time} seconds" + used_token = Monitor.get_token_usage() + if used_token: + trace_log_str += f", token usage: {used_token}" + log_trace(trace_log_str) + @wraps(func) async def wrapper_async(*args, **kwargs): # get llm input from arguments @@ -40,6 +81,9 @@ async def wrapper_async(*args, **kwargs): # add invocation chain to the monitor module. Monitor.add_invocation_chain({'source': source, 'type': 'llm'}) + log_trace(f"LLM input is: {llm_input}") + start_time = time.time() + if self and hasattr(self, 'tracing'): if self.tracing is False: return await func(*args, **kwargs) @@ -50,6 +94,13 @@ async def wrapper_async(*args, **kwargs): if isinstance(result, LLMOutput): # add llm invocation info to monitor Monitor().trace_llm_invocation(source=func.__qualname__, llm_input=llm_input, llm_output=result.text) + + # add llm token usage to monitor + trace_llm_token_usage(self, llm_input, result.text) + + log_llm_trace_output(result.text, start_time) + Monitor.pop_invocation_chain() + return result else: # streaming @@ -59,8 +110,14 @@ async def gen_iterator(): llm_output.append(chunk.text) yield chunk # add llm invocation info to monitor + output_str = "".join(llm_output) Monitor().trace_llm_invocation(source=func.__qualname__, llm_input=llm_input, - llm_output="".join(llm_output)) + llm_output=output_str) + # add llm token usage to monitor + trace_llm_token_usage(self, llm_input, output_str) + + log_llm_trace_output(output_str, start_time) + Monitor.pop_invocation_chain() return gen_iterator() @@ -82,6 +139,9 @@ def wrapper_sync(*args, **kwargs): # add invocation chain to the monitor module. Monitor.add_invocation_chain({'source': source, 'type': 'llm'}) + log_trace(f"LLM input is: {llm_input}") + start_time = time.time() + if self and hasattr(self, 'tracing'): if self.tracing is False: return func(*args, **kwargs) @@ -95,6 +155,8 @@ def wrapper_sync(*args, **kwargs): # add llm token usage to monitor trace_llm_token_usage(self, llm_input, result.text) + log_llm_trace_output(result.text, start_time) + Monitor.pop_invocation_chain() return result else: @@ -114,6 +176,9 @@ def gen_iterator(): # add llm token usage to monitor trace_llm_token_usage(self, llm_input, output_str) + log_llm_trace_output(output_str, start_time) + Monitor.pop_invocation_chain() + return gen_iterator() if asyncio.iscoroutinefunction(func): @@ -132,6 +197,52 @@ def _get_llm_input(func, *args, **kwargs) -> dict: return {k: v for k, v in bound_args.arguments.items()} +def get_caller_info(instance: object = None): + # 获取上一层调用者的帧 + func_name = "unknown func" + if instance is None: + frame = sys._getframe(2) + instance = frame.f_locals.get('self') # 获取调用者对象 + # 获取调用函数 + func_name = frame.f_code.co_name + + source = "" + type = "" + # 判断对象的类型是Agent、Tool、还是其他类型 + if hasattr(instance, 'component_type') and getattr(instance, 'component_type', None) == ComponentEnum.AGENT: + agent_model = getattr(instance, 'agent_model', None) + if isinstance(agent_model, object): + info = getattr(agent_model, 'info', None) + if isinstance(info, dict): + source = info.get('name', None) + type = 'agent' + elif hasattr(instance, 'component_type'): + component = getattr(instance, 'component_type', None) + if component == ComponentEnum.TOOL: + source = getattr(instance, 'name', None) + type = 'tool' + elif component == ComponentEnum.WORK_PATTERN: + # 智能体调用的work_pattern, frame需要向上找一层 + frame = sys._getframe(4) + return get_caller_info(frame.f_locals.get('self')) + elif component == ComponentEnum.KNOWLEDGE: + source = getattr(instance, 'name', None) + type = 'knowledge' + elif component == ComponentEnum.SERVICE: + source = getattr(instance, 'name', None) + type = 'user' + elif instance is not None: + source = instance.__class__.__qualname__ + type = "unknown" + else: + source = func_name + type = "unknown" + return { + 'source': source, + 'type': type + } + + def trace_agent(func): """Annotation: @trace_agent @@ -145,7 +256,6 @@ async def wrapper_async(*args, **kwargs): # check whether the tracing switch is enabled source = func.__qualname__ self = agent_input.pop('self', None) - tracing = None if isinstance(self, object): agent_model = getattr(self, 'agent_model', None) @@ -156,27 +266,31 @@ async def wrapper_async(*args, **kwargs): source = info.get('name', None) if isinstance(profile, dict): tracing = profile.get('tracing', None) - + start_info = get_caller_info() + pair_id = f"agent_{uuid.uuid4().hex}" + ConversationMemoryModule().add_agent_input_info(start_info, self, agent_input, pair_id) # add invocation chain to the monitor module. Monitor.add_invocation_chain({'source': source, 'type': 'agent'}) - if tracing is False: return await func(*args, **kwargs) - + kwargs['memory_source_info'] = start_info # invoke function result = await func(*args, **kwargs) # add agent invocation info to monitor Monitor().trace_agent_invocation(source=source, agent_input=agent_input, agent_output=result) + ConversationMemoryModule().add_agent_result_info(self, result, start_info, pair_id) return result @functools.wraps(func) def wrapper_sync(*args, **kwargs): + Monitor.init_trace_id() + Monitor.init_invocation_chain() + # get agent input from arguments agent_input = _get_input(func, *args, **kwargs) # check whether the tracing switch is enabled source = func.__qualname__ self = agent_input.pop('self', None) - tracing = None if isinstance(self, object): agent_model = getattr(self, 'agent_model', None) @@ -187,17 +301,29 @@ def wrapper_sync(*args, **kwargs): source = info.get('name', None) if isinstance(profile, dict): tracing = profile.get('tracing', None) - + pair_id = f"agent_{uuid.uuid4().hex}" + start_info = get_caller_info() + kwargs['memory_source_info'] = start_info + ConversationMemoryModule().add_agent_input_info(start_info, self, agent_input, pair_id) # add invocation chain to the monitor module. Monitor.add_invocation_chain({'source': source, 'type': 'agent'}) + log_trace(f"AGENT input is: {agent_input}") + start_time = time.time() + if tracing is False: return func(*args, **kwargs) # invoke function result = func(*args, **kwargs) # add agent invocation info to monitor + ConversationMemoryModule().add_agent_result_info(self, result, start_info, pair_id) Monitor().trace_agent_invocation(source=source, agent_input=agent_input, agent_output=result) + + cost_time = time.time() - start_time + log_trace(f"Agent output is:{result.to_json_str()}, cost {cost_time} seconds") + Monitor.pop_invocation_chain() + return result if asyncio.iscoroutinefunction(func): @@ -226,12 +352,15 @@ def wrapper_sync(*args, **kwargs): name = getattr(self, 'name', None) if name is not None: source = name - + start_info = get_caller_info() + pair_id = f"tool_{uuid.uuid4().hex}" + ConversationMemoryModule().add_tool_input_info(start_info, source, tool_input, pair_id) # add invocation chain to the monitor module. Monitor.add_invocation_chain({'source': source, 'type': 'tool'}) - + result = func(*args, **kwargs) + ConversationMemoryModule().add_tool_output_info(start_info, source, params=result, pair_id=pair_id) # invoke function - return func(*args, **kwargs) + return result # sync function return wrapper_sync @@ -256,11 +385,16 @@ def wrapper_sync(*args, **kwargs): if name is not None: source = name + start = get_caller_info() + pair_id = f"knowledge_{uuid.uuid4().hex}" + ConversationMemoryModule().add_knowledge_input_info(start, source, knowledge_input, pair_id) # add invocation chain to the monitor module. Monitor.add_invocation_chain({'source': source, 'type': 'knowledge'}) # invoke function - return func(*args, **kwargs) + result = func(*args, **kwargs) + ConversationMemoryModule().add_knowledge_output_info(start, source, params=result, pair_id=pair_id) + return result # sync function return wrapper_sync diff --git a/agentuniverse/base/component/component_configer_util.py b/agentuniverse/base/component/component_configer_util.py index 7b051cd8..004bce70 100644 --- a/agentuniverse/base/component/component_configer_util.py +++ b/agentuniverse/base/component/component_configer_util.py @@ -35,6 +35,7 @@ from agentuniverse.llm.llm_manager import LLMManager from agentuniverse.prompt.prompt_manager import PromptManager from agentuniverse.workflow.workflow_manager import WorkflowManager +from agentuniverse.base.util.logging.log_sink.log_sink_manager import LogSinkManager from agentuniverse.agent.action.knowledge.embedding.embedding_manager import EmbeddingManager from agentuniverse.agent.action.knowledge.doc_processor.doc_processor_manager import DocProcessorManager @@ -67,6 +68,7 @@ class ComponentConfigerUtil(object): ComponentEnum.MEMORY_COMPRESSOR: ComponentConfiger, ComponentEnum.MEMORY_STORAGE: ComponentConfiger, ComponentEnum.WORK_PATTERN: WorkPatternConfiger, + ComponentEnum.LOG_SINK: ComponentConfiger, ComponentEnum.DEFAULT: ComponentConfiger } @@ -90,6 +92,7 @@ class ComponentConfigerUtil(object): ComponentEnum.MEMORY_COMPRESSOR: MemoryCompressorManager, ComponentEnum.MEMORY_STORAGE: MemoryStorageManager, ComponentEnum.WORK_PATTERN: WorkPatternManager, + ComponentEnum.LOG_SINK: LogSinkManager } @classmethod diff --git a/agentuniverse/base/component/component_enum.py b/agentuniverse/base/component/component_enum.py index 5f69acbd..bbf0871c 100644 --- a/agentuniverse/base/component/component_enum.py +++ b/agentuniverse/base/component/component_enum.py @@ -31,6 +31,7 @@ class ComponentEnum(Enum): WORK_PATTERN = "WORK_PATTERN" MEMORY_COMPRESSOR = "MEMORY_COMPRESSOR" MEMORY_STORAGE = "MEMORY_STORAGE" + LOG_SINK = "LOG_SINK" @staticmethod def to_value_list(): diff --git a/agentuniverse/base/config/application_configer/app_configer.py b/agentuniverse/base/config/application_configer/app_configer.py index f0f173f7..355f6a70 100644 --- a/agentuniverse/base/config/application_configer/app_configer.py +++ b/agentuniverse/base/config/application_configer/app_configer.py @@ -37,6 +37,8 @@ def __init__(self): self.__core_memory_compressor_package_list: Optional[list[str]] = None self.__core_memory_storage_package_list: Optional[list[str]] = None self.__core_work_pattern_package_list: Optional[list[str]] = None + self.__core_log_sink_package_list: Optional[list[str]] = None + self.__conversation_memory_configer: Optional[dict] = None @property def base_info_appname(self) -> Optional[str]: @@ -145,6 +147,15 @@ def core_work_pattern_package_list(self) -> Optional[list[str]]: """Return the work pattern package list of the core.""" return self.__core_work_pattern_package_list + @property + def core_log_sink_package_list(self) -> Optional[list[str]]: + """Return the work pattern package list of the core.""" + return self.__core_log_sink_package_list + + @property + def conversation_memory_configer(self) -> dict: + return self.__conversation_memory_configer + def load_by_configer(self, configer: Configer) -> 'AppConfiger': """Load the AppConfiger by the given Configer. @@ -176,4 +187,6 @@ def load_by_configer(self, configer: Configer) -> 'AppConfiger': self.__core_memory_compressor_package_list = configer.value.get('CORE_PACKAGE', {}).get('memory_compressor') self.__core_memory_storage_package_list = configer.value.get('CORE_PACKAGE', {}).get('memory_storage') self.__core_work_pattern_package_list = configer.value.get('CORE_PACKAGE', {}).get('work_pattern') + self.__core_log_sink_package_list = configer.value.get('CORE_PACKAGE', {}).get('log_sink') + self.__conversation_memory_configer = configer.value.get('CONVERSATION_MEMORY') return self diff --git a/agentuniverse/base/config/component_configer/configers/memory_configer.py b/agentuniverse/base/config/component_configer/configers/memory_configer.py index cedfe783..c952ec28 100644 --- a/agentuniverse/base/config/component_configer/configers/memory_configer.py +++ b/agentuniverse/base/config/component_configer/configers/memory_configer.py @@ -25,6 +25,7 @@ def __init__(self, configer: Optional[Configer] = None): self.__memory_compressor: Optional[str] = None self.__memory_storages: Optional[List[str]] = None self.__memory_retrieval_storage: Optional[str] = None + self.__memory_summarize_agent: Optional[str] = None @property def name(self) -> Optional[str]: @@ -66,6 +67,11 @@ def memory_retrieval_storage(self) -> Optional[str]: """Return the retrieval storage of the Memory.""" return self.__memory_retrieval_storage + @property + def memory_summarize_agent(self) -> Optional[str]: + """Return the summarize agent of the Memory.""" + return self.__memory_summarize_agent + def load(self) -> 'MemoryConfiger': """Load the configuration by the Configer object. Returns: @@ -91,6 +97,7 @@ def load_by_configer(self, configer: Configer) -> 'MemoryConfiger': self.__memory_compressor = configer.value.get('memory_compressor') self.__memory_storages = configer.value.get('memory_storages') self.__memory_retrieval_storage = configer.value.get('memory_retrieval_storage') + self.__memory_summarize_agent = configer.value.get('memory_summarize_agent') except Exception as e: raise Exception(f"Failed to parse the Memory configuration: {e}") return self diff --git a/agentuniverse/base/config/configer.py b/agentuniverse/base/config/configer.py index 2fd3d3d3..656809d2 100644 --- a/agentuniverse/base/config/configer.py +++ b/agentuniverse/base/config/configer.py @@ -19,8 +19,7 @@ class PlaceholderResolver: def __init__(self): self._resolvers = [] self.register_resolver(r'\${(.+?)}', - lambda match: os.getenv(match.group(1), - match.group(0))) + lambda match: os.getenv(match.group(1), '')) def register_resolver(self, pattern, func): """Register a new resolver with a regex pattern and its corresponding function.""" self._resolvers.append((re.compile(pattern), func)) diff --git a/agentuniverse/base/util/agent_util.py b/agentuniverse/base/util/agent_util.py index cb56166c..1528c0b4 100644 --- a/agentuniverse/base/util/agent_util.py +++ b/agentuniverse/base/util/agent_util.py @@ -10,7 +10,7 @@ from agentuniverse.base.util.memory_util import get_memory_string -def assemble_memory_input(memory: Memory, agent_input: dict) -> list[Message]: +def assemble_memory_input(memory: Memory, agent_input: dict, query_params: dict = None) -> list[Message]: """Assemble memory information for the agent input parameters. Args: @@ -23,9 +23,12 @@ def assemble_memory_input(memory: Memory, agent_input: dict) -> list[Message]: memory_messages = [] if memory: # get the memory messages from the memory instance. - memory_messages = memory.get(**agent_input) + if not query_params: + memory_messages = memory.get(**agent_input) + else: + memory_messages = memory.get(**query_params) # convert the memory messages to a string and add it to the agent input object. - memory_str = get_memory_string(memory_messages) + memory_str = get_memory_string(memory_messages, agent_input.get('agent_id')) agent_input[memory.memory_key] = memory_str return memory_messages diff --git a/agentuniverse/base/util/logging/general_logger.py b/agentuniverse/base/util/logging/general_logger.py index 5a53331b..0145671b 100644 --- a/agentuniverse/base/util/logging/general_logger.py +++ b/agentuniverse/base/util/logging/general_logger.py @@ -9,9 +9,9 @@ from abc import ABC, abstractmethod import json from typing import Literal - import loguru +from agentuniverse.base.util.logging.log_type_enum import LogTypeEnum from agentuniverse.base.context.framework_context_manager import FrameworkContextManager LOG_LEVEL = Literal[ @@ -52,7 +52,8 @@ def _get_source_filter(source: str) -> callable: """ def source_filter(record) -> bool: - return record["extra"].get("source") == source + return record["extra"].get("log_type") == LogTypeEnum.default and \ + record["extra"].get("source") == source return source_filter @@ -60,6 +61,12 @@ def source_filter(record) -> bool: class Logger(ABC): """The basic class of all logger, define all level log functions.""" + def get_inheritance_depth(self): + """ + return the depth to base Logger + """ + return self.__class__.__mro__.index(Logger) + @property def _logger(self): """Logger field""" @@ -150,37 +157,43 @@ def update_properties(self, **kwargs): f"has no attribute '{key}'") def warn(self, msg, *args, **kwargs): - self._logger.opt(depth=1).bind( + self._logger.opt(depth=self.get_inheritance_depth()).bind( + log_type=LogTypeEnum.default, source=self.module_name, context_prefix=_get_context_prefix() ).warning(msg, *args, **kwargs) def info(self, msg, *args, **kwargs): - self._logger.opt(depth=1).bind( + self._logger.opt(depth=self.get_inheritance_depth()).bind( + log_type=LogTypeEnum.default, source=self.module_name, context_prefix=_get_context_prefix() ).info(msg, *args, **kwargs) def error(self, msg, *args, **kwargs): - self._logger.opt(depth=1).bind( + self._logger.opt(depth=self.get_inheritance_depth()).bind( + log_type=LogTypeEnum.default, source=self.module_name, context_prefix=_get_context_prefix() ).error(msg, *args, **kwargs) def critical(self, msg, *args, **kwargs): - self._logger.opt(depth=1).bind( + self._logger.opt(depth=self.get_inheritance_depth()).bind( + log_type=LogTypeEnum.default, source=self.module_name, context_prefix=_get_context_prefix() ).critical(msg, *args, **kwargs) def trace(self, msg, *args, **kwargs): - self._logger.opt(depth=1).bind( + self._logger.opt(depth=self.get_inheritance_depth()).bind( + log_type=LogTypeEnum.default, source=self.module_name, context_prefix=_get_context_prefix() ).trace(msg, *args, **kwargs) def debug(self, msg, *args, **kwargs): - self._logger.opt(depth=1).bind( + self._logger.opt(depth=self.get_inheritance_depth()).bind( + log_type=LogTypeEnum.default, source=self.module_name, context_prefix=_get_context_prefix() ).debug(msg, *args, **kwargs) diff --git a/agentuniverse/base/util/logging/log_sink/__init__.py b/agentuniverse/base/util/logging/log_sink/__init__.py new file mode 100644 index 00000000..272c72de --- /dev/null +++ b/agentuniverse/base/util/logging/log_sink/__init__.py @@ -0,0 +1,7 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/12/9 17:58 +# @Author : fanen.lhy +# @Email : fanen.lhy@antgroup.com +# @FileName: __init__.py.py diff --git a/agentuniverse/base/util/logging/log_sink/base_file_log_sink.py b/agentuniverse/base/util/logging/log_sink/base_file_log_sink.py new file mode 100644 index 00000000..5e238de1 --- /dev/null +++ b/agentuniverse/base/util/logging/log_sink/base_file_log_sink.py @@ -0,0 +1,57 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/12/9 18:01 +# @Author : fanen.lhy +# @Email : fanen.lhy@antgroup.com +# @FileName: base_log_sink.py +from loguru import logger + +from agentuniverse.base.config.component_configer.component_configer import \ + ComponentConfiger +from agentuniverse.base.util.logging.log_sink.log_sink import LogSink +from agentuniverse.base.util.logging.logging_config import LoggingConfig +from agentuniverse.base.util.logging.logging_util import _get_log_file_path + + +class BaseFileLogSink(LogSink): + + file_prefix: str = None + log_rotation: str = LoggingConfig.log_rotation + log_retention: str = LoggingConfig.log_retention + compression: str = None + + def process_record(self, record): + raise NotImplementedError("Subclasses must implement process_record.") + + def filter(self, record): + if not record['extra'].get('log_type') == self.log_type: + return False + self.process_record(record) + return True + + def register_sink(self): + if self.sink_id == -1: + self.sink_id = logger.add( + sink=_get_log_file_path(self.file_prefix), + level=self.level, + format=self.format, + filter=self.filter, + rotation=self.log_rotation, + retention=self.log_retention, + compression=self.compression, + encoding="utf-8", + enqueue=self.enqueue + ) + + def _initialize_by_component_configer(self, + log_sink_configer: ComponentConfiger) -> 'LogSink': + if hasattr(log_sink_configer, "file_prefix"): + self.file_prefix = log_sink_configer.file_prefix + if hasattr(log_sink_configer, "log_rotation"): + self.log_rotation = log_sink_configer.log_rotation + if hasattr(log_sink_configer, "log_retention"): + self.log_retention = log_sink_configer.log_retention + if hasattr(log_sink_configer, "compression"): + self.compression = log_sink_configer.compression + return self diff --git a/agentuniverse/base/util/logging/log_sink/flask_request_log_sink.py b/agentuniverse/base/util/logging/log_sink/flask_request_log_sink.py new file mode 100644 index 00000000..38d45d9c --- /dev/null +++ b/agentuniverse/base/util/logging/log_sink/flask_request_log_sink.py @@ -0,0 +1,30 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/12/9 18:01 +# @Author : fanen.lhy +# @Email : fanen.lhy@antgroup.com +# @FileName: base_log_sink.py + +from agentuniverse.base.util.logging.log_sink.base_file_log_sink import BaseFileLogSink +from agentuniverse.base.util.logging.log_type_enum import LogTypeEnum + + +class FlaskRequestLogSink(BaseFileLogSink): + log_type: LogTypeEnum = LogTypeEnum.flask_request + + def filter(self, record): + if not record['extra'].get('log_type') == self.log_type: + return False + self.process_record(record) + return True + + def process_record(self, record): + record["message"] = self.generate_log( + flask_request=record['extra']['flask_request'] + ) + record['extra'].pop('flask_request', None) + + + def generate_log(self, flask_request) -> str: + pass diff --git a/agentuniverse/base/util/logging/log_sink/flask_response_log_sink.py b/agentuniverse/base/util/logging/log_sink/flask_response_log_sink.py new file mode 100644 index 00000000..d0525590 --- /dev/null +++ b/agentuniverse/base/util/logging/log_sink/flask_response_log_sink.py @@ -0,0 +1,30 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/12/9 18:01 +# @Author : fanen.lhy +# @Email : fanen.lhy@antgroup.com +# @FileName: base_log_sink.py + +from agentuniverse.base.util.logging.log_sink.base_file_log_sink import BaseFileLogSink +from agentuniverse.base.util.logging.log_type_enum import LogTypeEnum + + +class FlaskResponseLogSink(BaseFileLogSink): + log_type: LogTypeEnum = LogTypeEnum.flask_response + + def filter(self, record): + if not record['extra'].get('log_type') == self.log_type: + return False + self.process_record(record) + return True + + def process_record(self, record): + record["message"] = self.generate_log( + flask_response=record['extra'].get('flask_response'), + elapsed_time=record['extra']['elapsed_time'] + ) + record['extra'].pop('flask_response', None) + + def generate_log(self, flask_response, elapsed_time) -> str: + pass diff --git a/agentuniverse/base/util/logging/log_sink/log_sink.py b/agentuniverse/base/util/logging/log_sink/log_sink.py new file mode 100644 index 00000000..10248c33 --- /dev/null +++ b/agentuniverse/base/util/logging/log_sink/log_sink.py @@ -0,0 +1,77 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/12/9 18:01 +# @Author : fanen.lhy +# @Email : fanen.lhy@antgroup.com +# @FileName: base_log_sink.py +from typing import Optional +from loguru import logger + +from agentuniverse.base.util.logging.log_type_enum import LogTypeEnum +from agentuniverse.base.util.logging.logging_config import LoggingConfig +from agentuniverse.base.component.component_base import ComponentEnum +from agentuniverse.base.component.component_base import ComponentBase +from agentuniverse.base.config.component_configer.component_configer import \ + ComponentConfiger + + + +class LogSink(ComponentBase): + """The basic class for log sink. + """ + + component_type: ComponentEnum = ComponentEnum.LOG_SINK + name: Optional[str] = None + description: Optional[str] = None + level: str = "INFO" + format: str = LoggingConfig.log_format + sink_id: int = -1 + log_type: LogTypeEnum = LogTypeEnum.default + enqueue: bool = True + + class Config: + arbitrary_types_allowed = True + + def get_inheritance_depth(self): + """ + return the depth to base Logger + """ + return self.__class__.__mro__.index(LogSink) + + def __call__(self, message): + self.process_record(message.record) + + def process_record(self, record): + raise NotImplementedError("Subclasses must implement process_record.") + + def filter(self, record): + if not record['extra'].get('log_type') == self.log_type: + return False + return True + + def register_sink(self): + if self.sink_id == -1: + self.sink_id = logger.add( + self, + level=self.level, + format=self.format, + filter=self.filter, + enqueue=self.enqueue + ) + + def initialize_by_component_configer(self, + log_sink_configer: ComponentConfiger) -> 'LogSink': + self.name = log_sink_configer.name + self.description = log_sink_configer.description + if hasattr(log_sink_configer, "level"): + self.level = log_sink_configer.level + if hasattr(log_sink_configer, "format"): + self.format = log_sink_configer.format + if hasattr(log_sink_configer, "enqueue"): + self.enqueue = log_sink_configer.enqueue + + self._initialize_by_component_configer(log_sink_configer) + + self.register_sink() + return self diff --git a/agentuniverse/base/util/logging/log_sink/log_sink_manager.py b/agentuniverse/base/util/logging/log_sink/log_sink_manager.py new file mode 100644 index 00000000..6414013c --- /dev/null +++ b/agentuniverse/base/util/logging/log_sink/log_sink_manager.py @@ -0,0 +1,20 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/12/9 18:06 +# @Author : fanen.lhy +# @Email : fanen.lhy@antgroup.com +# @FileName: log_sink_manager.py +from agentuniverse.base.annotation.singleton import singleton +from agentuniverse.base.component.component_enum import ComponentEnum +from agentuniverse.base.component.component_manager_base import ComponentManagerBase +from agentuniverse.base.util.logging.log_sink.log_sink import LogSink + + +@singleton +class LogSinkManager(ComponentManagerBase[LogSink]): + """A singleton manager class of the DocProcessor.""" + + def __init__(self): + super().__init__(ComponentEnum.LOG_SINK) + \ No newline at end of file diff --git a/agentuniverse/base/util/logging/log_type_enum.py b/agentuniverse/base/util/logging/log_type_enum.py new file mode 100644 index 00000000..127d8ec8 --- /dev/null +++ b/agentuniverse/base/util/logging/log_type_enum.py @@ -0,0 +1,15 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/12/5 16:22 +# @Author : fanen.lhy +# @Email : fanen.lhy@antgroup.com +# @FileName: log_type_const.py +from enum import Enum + + +class LogTypeEnum(str, Enum): + default = 'default' + llm_trace = 'llm_trace' + flask_request = 'flask_request' + flask_response = 'flask_response' diff --git a/agentuniverse/base/util/logging/logging_config.py b/agentuniverse/base/util/logging/logging_config.py index 4a18e119..b4d1a71a 100644 --- a/agentuniverse/base/util/logging/logging_config.py +++ b/agentuniverse/base/util/logging/logging_config.py @@ -9,7 +9,7 @@ from typing import Optional, List, Dict import tomli - +from agentuniverse.base.config.configer import Configer def _load_toml_file(path: str) -> dict: """Load the toml file. @@ -39,6 +39,7 @@ class LoggingConfig(object): log_path: Optional[str] = None log_rotation: str = "10 MB" log_retention: str = "3 days" + log_compression: str = "zip" # Aliyun sls configs. sls_endpoint: str = "" @@ -59,7 +60,7 @@ def __init__(self, config_path: Optional[str] = None): """ self.__config = None try: - self.__config = _load_toml_file(config_path)["LOG_CONFIG"] + self.__config = Configer().load_by_path(config_path).value['LOG_CONFIG'] except (FileNotFoundError, TypeError): print("can't find log config file, use default config") for log_module in LoggingConfig.log_extend_module_list: @@ -99,6 +100,11 @@ def __init__(self, config_path: Optional[str] = None): if log_retention: LoggingConfig.log_retention = log_retention + log_compress = self._get_config_or_default("BASIC_CONFIG", + "log_compression") + if log_compress is not None: + LoggingConfig.log_compression = log_compress + # Read sls config when sls extend module come into effect. if LoggingConfig.log_extend_module_switch["sls_log"]: LoggingConfig.sls_endpoint = self._get_config_or_default( diff --git a/agentuniverse/base/util/logging/logging_util.py b/agentuniverse/base/util/logging/logging_util.py index e937474b..952e5e77 100644 --- a/agentuniverse/base/util/logging/logging_util.py +++ b/agentuniverse/base/util/logging/logging_util.py @@ -10,12 +10,14 @@ import sys from typing import Optional from pathlib import Path +from typing_extensions import deprecated import loguru from .general_logger import GeneralLogger, LOG_LEVEL from .logging_config import LoggingConfig, init_log_config from ..system_util import get_project_root_path +from agentuniverse.base.util.logging.log_type_enum import LogTypeEnum _module_logger_dict = {} STANDARD_LOG_SUFFIX = 'all' @@ -25,6 +27,10 @@ LOGGER = GeneralLogger(STANDARD_LOG_SUFFIX, "", "", "", "", add_handler=False) +def _standard_filter(record): + return record["extra"].get('log_type') == LogTypeEnum.default + + def _get_log_file_path(log_suffix: str) -> str: """Get full log file path by contacting log save path and log file name. If log path is not specified, create a subdir to save logs under project @@ -49,6 +55,7 @@ def _get_log_file_path(log_suffix: str) -> str: def _add_standard_logger(): """Add a standard loguru handler.""" + LOGGER.update_properties( log_path=_get_log_file_path(STANDARD_LOG_SUFFIX), log_format=LoggingConfig.log_format, @@ -58,9 +65,10 @@ def _add_standard_logger(): sink=_get_log_file_path(STANDARD_LOG_SUFFIX), level=LoggingConfig.log_level, format=LoggingConfig.log_format, + filter=_standard_filter, rotation=LoggingConfig.log_rotation, retention=LoggingConfig.log_retention, - compression='zip', + compression=LoggingConfig.log_compression if LoggingConfig.log_compression else None, encoding="utf-8", enqueue=True ) @@ -72,6 +80,7 @@ def _add_std_out_handler(): sink=sys.stdout, level=LoggingConfig.log_level, format=LoggingConfig.log_format, + filter=_standard_filter, enqueue=True ) @@ -137,6 +146,7 @@ def get_module_logger(module_name: str, return new_logger +@deprecated('Deleted in future, use LogSink to register a new sink instead.') def add_sink(sink, log_level: Optional[LOG_LEVEL] = None) -> bool: """Validate the given sink and add it to the loguru logger if valid. diff --git a/agentuniverse/base/util/memory_util.py b/agentuniverse/base/util/memory_util.py index 4e39489c..2346b487 100644 --- a/agentuniverse/base/util/memory_util.py +++ b/agentuniverse/base/util/memory_util.py @@ -11,6 +11,7 @@ from agentuniverse.agent.memory.enum import ChatMessageEnum from agentuniverse.agent.memory.message import Message +from agentuniverse.base.context.framework_context_manager import FrameworkContextManager from agentuniverse.llm.llm import LLM from agentuniverse.llm.llm_manager import LLMManager @@ -43,16 +44,17 @@ def generate_memories(chat_messages: BaseChatMessageHistory) -> list: ] if chat_messages.messages else [] -def get_memory_string(messages: List[Message]) -> str: +def get_memory_string(messages: List[Message], agent_id=None) -> str: """Convert the given messages to a string. Args: messages(List[Message]): The list of messages. + Returns: str: The string representation of the messages. """ - + current_trace_id = FrameworkContextManager().get_context("trace_id") string_messages = [] for m in messages: if m.type == ChatMessageEnum.SYSTEM.value: @@ -61,14 +63,26 @@ def get_memory_string(messages: List[Message]) -> str: role = 'Human' elif m.type == ChatMessageEnum.AI.value: role = "AI" + elif m.type == ChatMessageEnum.INPUT.value or m.type == ChatMessageEnum.OUTPUT.value: + if current_trace_id == m.trace_id: + continue + role: str = m.metadata.get('prefix', "") + if agent_id: + role = role.replace(f"智能体 {agent_id}", " 你") + role = role.replace(f"Agent {agent_id}", " You") + m_str = f"{m.metadata.get('timestamp')} {role}:{m.content}" + string_messages.append(m_str) + continue else: role = "" m_str = "" + if m.metadata and m.metadata.get('gmt_created'): + m_str += f"{m.metadata.get('gmt_created')} " + if m.source: + m_str += f" Message source: {m.source} " if role: m_str += f"Message role: {role} " - if m.source: - m_str += f"Message source: {m.source} " - m_str += f"Message content: \n {m.content} " + m_str += f" :{m.content} " string_messages.append(m_str) return "\n\n".join(string_messages) diff --git a/agentuniverse/base/util/monitor/monitor.py b/agentuniverse/base/util/monitor/monitor.py index 1dfdc396..6e638625 100644 --- a/agentuniverse/base/util/monitor/monitor.py +++ b/agentuniverse/base/util/monitor/monitor.py @@ -102,6 +102,18 @@ def init_invocation_chain(): if FrameworkContextManager().get_context(trace_id + '_invocation_chain') is None: FrameworkContextManager().set_context(trace_id + '_invocation_chain', []) + @staticmethod + def pop_invocation_chain(): + """Pop the last chain node in invocation chain.""" + trace_id = FrameworkContextManager().get_context('trace_id') + if trace_id is not None: + invocation_chain: list = FrameworkContextManager().get_context( + trace_id + '_invocation_chain') + if invocation_chain is not None: + invocation_chain.pop() + FrameworkContextManager().set_context( + trace_id + '_invocation_chain', invocation_chain) + @staticmethod def clear_invocation_chain(): """Clear the invocation chain in the framework context.""" diff --git a/docs/guidebook/_picture/conversation_memory_flow.jpg b/docs/guidebook/_picture/conversation_memory_flow.jpg new file mode 100644 index 00000000..bc501f34 Binary files /dev/null and b/docs/guidebook/_picture/conversation_memory_flow.jpg differ diff --git a/docs/guidebook/_picture/memory_trace_time_flow.png b/docs/guidebook/_picture/memory_trace_time_flow.png new file mode 100644 index 00000000..04d62ddd Binary files /dev/null and b/docs/guidebook/_picture/memory_trace_time_flow.png differ diff --git a/docs/guidebook/en/In-Depth_Guides/Tech_Capabilities/Deployment/Docker_Container_Deployment.md b/docs/guidebook/en/In-Depth_Guides/Tech_Capabilities/Deployment/Docker_Container_Deployment.md index bdfd55ed..4f251f7a 100644 --- a/docs/guidebook/en/In-Depth_Guides/Tech_Capabilities/Deployment/Docker_Container_Deployment.md +++ b/docs/guidebook/en/In-Depth_Guides/Tech_Capabilities/Deployment/Docker_Container_Deployment.md @@ -6,14 +6,14 @@ agentUniverse provides standard work environment images for the containerized de 1. Build your own project according to the standard directory structure of agentUniverse, referring to the [Application_Engineering_Structure_Explanation](../../../Get_Start/1.Application_Project_Structure_and_Explanation.md). For ease of explanation, this document assumes the project name and project directory are `sample_standard_app`. 2. Obtain the required version of the AagentUniverse image. ```shell -docker pull registry.cn-hangzhou.aliyuncs.com/agent_universe/agent_universe:0.0.9_centos8 +docker pull registry.cn-hangzhou.aliyuncs.com/agent_universe/agent_universe:0.0.14b1_centos8 ``` ## Method 1: Mount the host path You can mount your project to a path inside the container by mounting the host directory. The reference command is as follows: ```shell -docker run -d -p 8888:8888 -v ./sample_standard_app/:/usr/local/etc/workspace/project/sample_standard_app registry.cn-hangzhou.aliyuncs.com/agent_universe/agent_universe:0.0.9_centos8 +docker run -d -p 8888:8888 -v ./sample_standard_app/:/usr/local/etc/workspace/project/sample_standard_app registry.cn-hangzhou.aliyuncs.com/agent_universe/agent_universe:0.0.14b1_centos8 ``` The`-p 8888:8888`represents the port mapping for the Web Server. The first 8888 indicates that the web server inside the container is started on port 8888, and the latter indicates that it is mapped to port 8888 on the host machine. Adjust it according to the actual startup conditions of your application as needed. `-v {local_dir}:/usr/local/etc/workspace/project/{local_dir_name}`indicates that the `local_dir` directory on the host is mounted to the `/usr/local/etc/workspace/project`within the container. The directory path inside the container is a fixed value and cannot be modified. `local_dir_name` stands for the last pattern of the `local_dir`. @@ -34,7 +34,7 @@ If you need multiple containers to mount the same directory, consider the follow ## Method 2: Pull the project from Github The image already has the git command installed. You can modify the image's entrypoint to git clone your project and then copy the entire project to a specified path. For example: ```shell -docker run -d -p 8888:8888 --entrypoint=/bin/bash registry.cn-hangzhou.aliyuncs.com/agent_universe/agent_universe:0.0.5_centos8_beta -c "git clone {repo_addr}; mv {project_dir} /usr/local/etc/workspace/project; /bin/bash --login /usr/local/etc/workspace/shell/start.sh" +docker run -d -p 8888:8888 --entrypoint=/bin/bash registry.cn-hangzhou.aliyuncs.com/agent_universe/agent_universe:0.0.14b1_centos8 -c "git clone {repo_addr}; mv {project_dir} /usr/local/etc/workspace/project; /bin/bash --login /usr/local/etc/workspace/shell/start.sh" ```` Where `repo_addr` is the address of your git project,and `project_dir` is the project directory, for example,if `sample_standard_app` is in the `project`directory within your git project, then`project_dir` would be `project/sample_standard_app`。 ## Result Verification diff --git "a/docs/guidebook/zh/How-to/\345\256\232\344\271\211\344\270\216\344\275\277\347\224\250\345\205\250\345\261\200\350\256\260\345\277\206/\345\246\202\344\275\225\344\275\277\347\224\250\345\205\250\345\261\200\350\256\260\345\277\206.md" "b/docs/guidebook/zh/How-to/\345\256\232\344\271\211\344\270\216\344\275\277\347\224\250\345\205\250\345\261\200\350\256\260\345\277\206/\345\246\202\344\275\225\344\275\277\347\224\250\345\205\250\345\261\200\350\256\260\345\277\206.md" new file mode 100644 index 00000000..99f701d8 --- /dev/null +++ "b/docs/guidebook/zh/How-to/\345\256\232\344\271\211\344\270\216\344\275\277\347\224\250\345\205\250\345\261\200\350\256\260\345\277\206/\345\246\202\344\275\225\344\275\277\347\224\250\345\205\250\345\261\200\350\256\260\345\277\206.md" @@ -0,0 +1,80 @@ +# 如何使用全局记忆 + +## 如何启动自动记忆采集 +- 修改全局配置文件,在config.toml中添加如何配置 +```toml +[CONVERSATION_MEMORY] +# Whether to activate conversation memory. +activate = true +# The name of the memory. +instance_name = 'global_conversation_memory' +# Whether to enable logging. +logging = true +# The language of the memory cn/en. +conversation_format = 'cn' +# The types you want to collection +collection_types = ['llm','tool','agent','knowledge'] +``` +配置说明 + +| 名称 | 类型 | 说明 | +|-----------------------|--------|----------------------------------------------------------------------------------| +| `activate` | bool | 全局启用开关,控制是否开启记忆采集功能,默认关闭。 | +| `logging` | bool | 是否在执行过程中动态打印记忆信息的日志,便于调试和监控。 | +| `collection_type` | list | 要采集的记忆的内容类型列表,如`['user', 'agent', 'tool', 'llm', 'knowledge']`,用于指定哪些类型的交互需要被记录。 | +| `conversation_format` | string | 指定会话记忆的格式化语言设置,影响记忆内容的表示方式。 | +| `instance_name` | string | 全局记忆的存储、压缩等配置的实例对象名称,标识特定配置下的记忆库。 | + +- 修改智能体配置文件,一份使用全局配置的智能体配置示例如下: +```yaml +info: + name: 'rag_agent_case' + description: 'rag agent case with conversation memory' +profile: + introduction: 你是智能体聊天助手。 + target: 你的任务是基于我们之前的对话和我提供的新信息来帮助用户解决问题。记住,我们要确保对话是自然流畅的,并且尽可能地准确和有用。 + instruction: | + 背景知识: + {background} + + 之前的对话: + {chat_history} + + ------------------------------------- + 现在请根据以上所有信息,请给出一个既符合对话上下文又利用了最新背景知识的答案。如果你不确定某些细节,坦诚告知用户你不知道,并尝试提供一个一般性的指导或者建议进一步查询的方向。 + 需要回答的问题是: {input} + llm_model: + name: 'qwen_llm' + model_name: 'qwen2.5-72b-instruct' + temperature: 0.1 +action: + tool: + - 'google_search_tool' + knowledge: +memory: + conversation_memory: 'global_conversation_memory' + input_field: 'input' + output_field: 'output' + top_k: 10 + collection_types: [ 'llm','tool','agent','knowledge' ] + memory_type: [ 'agent','tool' ] + auto_trace: true + +metadata: + type: 'AGENT' + module: 'demo_startup_app_with_single_agent_and_memory.intelligence.agentic.agent.agent_instance.rag_agent_case_template' + class: 'RagAgentCaseTemplate' +``` +配置说明: + +| 名称 | 类型 | 说明 | +|-----------------------|--------|------------------------------------------------| +| `conversation_memory` | string | 你想要使用的记忆库名称,用户全局记忆同步到智能体记忆中。 与全局记忆配置相同时,不会触发同步 | +| `input_field` | string | 需要采集的输入字段,不配置会采集所有输入 | +| `output_field` | string | 需要采集的输出字段,不配置会采集所有输出 | +| `top_k` | int | 记忆查询时返回的记忆条数,默认为10条 | +| `collection_types` | list | 当前智能体记忆采集的内容类型,默认为采集 | +| `memory_type` | list | 当前智能体需要使用的记忆类型,默认为自己的QA | +| `auto_trace` | bool | 是否自动追踪记忆,默认为True | + +单个智能体,不想使用全局记忆,可以将auto_trace设置为False, 此时不会自动采集任何与该智能体相关的记忆内容 \ No newline at end of file diff --git "a/docs/guidebook/zh/In-Depth_Guides/\345\216\237\347\220\206\344\273\213\347\273\215/\350\256\260\345\277\206/\345\205\250\345\261\200\350\256\260\345\277\206.md" "b/docs/guidebook/zh/In-Depth_Guides/\345\216\237\347\220\206\344\273\213\347\273\215/\350\256\260\345\277\206/\345\205\250\345\261\200\350\256\260\345\277\206.md" new file mode 100644 index 00000000..2165e4a5 --- /dev/null +++ "b/docs/guidebook/zh/In-Depth_Guides/\345\216\237\347\220\206\344\273\213\347\273\215/\350\256\260\345\277\206/\345\205\250\345\261\200\350\256\260\345\277\206.md" @@ -0,0 +1,116 @@ +# 记忆概念说明 + +## 单个智能体的记忆 + +单个智能体的记忆是指该智能体在与用户、其他智能体、工具以及知识库交互过程中所记录的内容。这些交互内容包括以下要素: + +- **消息类型 (`type`)**: 指消息是输入(`input`)还是输出(`output`),即该消息对当前智能体而言是询问还是回答。 +- **消息发起方 (`source`)**: 标识消息是由哪个实体发出的,可以是用户(`user`)、智能体(`agent_name`)或工具(`tool`) + 。例如,当用户向智能体提出问题时,`source`为`user`且`type`为`input`;而当智能体作出回应时,`source`为智能体,`type`为 + `output`。 +- **消息接收方 (`target`)**: 指明消息的接收者,可能是用户(`user`)、智能体(`agent_name`)或工具(`tool`)。这与消息的流向相关联,例如,用户的问题将 + `target`设为智能体,而智能体的回答则将`target`设为用户。 +- **发起方类型 (`source_type`)**: 描述发起方的种类,如智能体(`agent`)、工具(`tool`)、用户(`user`)、语言模型(`llm`)或知识库( + `knowledge`)。 +- **接收方类型 (`target_type`)**: 表示接收方的类别,选项同`source_type`。 +- **消息内容 (`content`)**: 使用自然语言描述的消息主体,比如: + - 用户向智能体`demo_rag_agent`提问:“巴菲特退出比亚迪的原因是什么?” + - `demo_rag_agent`回答了用户的问题:“巴菲特退出比亚迪的原因是xxxxxx。” + +通过`source`与`target`属性标记消息的流向,并利用`source_type`与`target_type`来指定流向的类型。 + +## 记忆的主要关联方 + +记忆中的交互涉及以下几个主要关联方: + +- **用户 (`user`)**: 发起请求或接收响应的人类用户。 +- **工具 (`tool`)**: 可能被智能体使用以辅助完成任务的应用程序或设备。 +- **知识库 (`knowledge`)**: 提供信息或数据的存储库,如数据库、文本文件、网页等。 + +## 记忆的主要操作 + +新增、检索、删除、压缩、裁剪 + +## 全局记忆 + +在多智能体系统中,记忆不仅记录单个智能体的交互信息,还按时间顺序和`trace_id` +记录所有参与智能体的工作流程。这使得可以从整体协作的角度追溯每个智能体的行为,并还原出单个智能体的记忆。 + +### 多智能体交互记录要素 + +- **会话标识 (`conversation_id`)**: 用于标识消息属于哪个会话,适用于跨越多个轮次的长对话。 +- **追踪标识 (`trace_id`)**: 用于记录当前轮次的信息,确保可以跟踪每一轮次中的具体交互。 + +通过上述要素,特别是`conversation_id`和`trace_id`,可以精确地记录和重现多智能体间的交互过程,以及各智能体内部的状态变化。 + +### 全局记忆到智能体记忆同步机制 + +同步过程是全局记忆管理的一个关键部分,它确保了智能体能够及时获得最新的相关信息。当全局记忆更新时,系统会识别出哪些变化与特定智能体相关,并将这些变化同步到智能体的本地记忆库中。这一过程是自动化的,旨在最小化延迟并最大化智能体间的协作效率。 + +## 记忆的主要关联方 + +记忆中的交互涉及以下几个主要关联方: + +- **用户 (`user`)**: 发起请求或接收响应的人类用户。 +- **智能体 (`agent`)**: 参与交互处理并响应其他实体的实体。 +- **工具 (`tool`)**: 可能被智能体使用以辅助完成任务的应用程序或设备。 +- **知识 (`knowledge`)**: 智能体可以访问的知识资源,可能用于增强决策或提供信息。 + +## 全局记忆的数据结构 + +为了记录和管理多智能体系统中的交互,我们定义了全局记忆的数据结构,用于捕捉每次交互的关键细节。以下是该数据结构中每个字段的具体说明: + +| 名称 | 类型 | 说明 | +|-------------------|--------|---------------------------------------------------------------------------------------------| +| `id` | string | 唯一标识符,确保每条记录在全局记忆库中是独一无二的。 | +| `conversation_id` | string | 标识消息所属的会话,适用于跨越多个轮次的长对话。 | +| `trace_id` | string | 在一次交互过程中,所有相关工具、智能体、知识库调用的消息拥有相同的`trace_id`,以便于追踪单轮次内的所有活动。 | +| `source` | string | 交互的发起方,可以是用户(`user`)、智能体(`agent`)或工具(`tool`)等。 | +| `source_type` | string | 发起方的类型,进一步明确`source`的具体类别,如智能体(`agent`)、工具(`tool`)、用户(`user`)、语言模型(`llm`)或知识库(`knowledge`)。 | +| `target` | string | 消息的接收方,与`source`相对,表示消息的目标实体。 | +| `target_type` | string | 接收方的类型,与`source_type`类似,但针对的是接收方。 | +| `content` | string | 消息的内容,使用自然语言表达,例如用户的提问或智能体的回答。 | +| `type` | string | 消息的类型,分为输入(`input`)和输出(`output`),即询问还是回答。 | +| `timestamp` | string | 消息的时间戳,采用标准时间格式记录消息发生的时间,有助于按时间顺序组织记忆。 | + +### 数据结构的作用 + +通过上述字段,`conversation memory` +能够全面记录每一次交互的详细信息,包括但不限于交互双方的身份、交互内容以及交互发生的背景(如会话ID和时间)。这些记录不仅支持全局记忆库的维护,还为后续的分析、检索和同步操作提供了坚实的基础。 + +# 详细能力设计 + +## 记忆采集的流程 + +### 记忆采集的时序过程 + +![memory_trace_time_flow.png](../../../../_picture/memory_trace_time_flow.png) + +### 全局记忆采集流程 + +![memory_trace_flow.png](../../../../_picture/conversation_memory_flow.jpg) + +# 记忆采集流程 + +为了确保多智能体系统中的交互能够被高效、准确地记录下来,我们设计了一套记忆采集流程。该流程通过异步线程、阻塞队列和切面配置来实现自动化记忆管理。以下是详细的步骤说明: + +### 1. 异步线程启动与队列初始化 + +- **服务启动后**:当多智能体系统启动时,会创建一个独立的异步线程。 +- **阻塞消费队列**:该线程负责监控并从队列中消费消息,采用阻塞模式等待新消息的到来。 + +### 2. 检测线程环境变量并推送信息到队列 + +- **调用工具、智能体或大模型之前/之后**:在每次调用这些组件前或接收其结果后,系统会检查当前交互智能体的配置,判断当前交互是否需要存储。 + +### 3. 阻塞队列消费与存储 + +- **消费消息**:异步线程持续从队列中消费消息,处理每个条目。 +- **存储到memory**:将处理后的调用信息存储到全局记忆库(`memory`)中,确保所有交互都被准确记录。 + +### 4. 标准aU组件的切面配置 + +- **配置切面**:所有标准的aU组件(例如智能体、工具、知识库)都预先配置了切面(AOP, Aspect-Oriented Programming),用于拦截特定方法的执行。 +- **记忆采集操作**:在切面中,系统会根据用户是否启用了全局记忆功能来决定是否执行记忆采集操作。如果启用,则按照前述流程进行记忆记录;否则,跳过记忆采集步骤。 + + diff --git "a/docs/guidebook/zh/In-Depth_Guides/\346\212\200\346\234\257\347\273\204\344\273\266/\351\203\250\347\275\262\350\277\220\347\273\264/Docker\345\256\271\345\231\250\345\214\226\351\203\250\347\275\262.md" "b/docs/guidebook/zh/In-Depth_Guides/\346\212\200\346\234\257\347\273\204\344\273\266/\351\203\250\347\275\262\350\277\220\347\273\264/Docker\345\256\271\345\231\250\345\214\226\351\203\250\347\275\262.md" index 3424a3bf..770df25f 100644 --- "a/docs/guidebook/zh/In-Depth_Guides/\346\212\200\346\234\257\347\273\204\344\273\266/\351\203\250\347\275\262\350\277\220\347\273\264/Docker\345\256\271\345\231\250\345\214\226\351\203\250\347\275\262.md" +++ "b/docs/guidebook/zh/In-Depth_Guides/\346\212\200\346\234\257\347\273\204\344\273\266/\351\203\250\347\275\262\350\277\220\347\273\264/Docker\345\256\271\345\231\250\345\214\226\351\203\250\347\275\262.md" @@ -6,14 +6,14 @@ AgentUniverse提供标准的工作环境镜像用于容器化部署AgentUniverse 1. 按照AgentUniverse的标准结构目录搭建自己的项目,具体结构参考[应用工程结构及说明](../../../开始使用/1.标准应用工程结构说明.md)。为方便说明,在本文档中假设项目名称和工程目录为`sample_standard_app`。 2. 获取所需版本的镜像: ```shell -docker pull registry.cn-hangzhou.aliyuncs.com/agent_universe/agent_universe:0.0.9_centos8 +docker pull registry.cn-hangzhou.aliyuncs.com/agent_universe/agent_universe:0.0.14b1_centos8 ``` ## 方法一:挂载宿主机路径 您可以通过将宿主机目录挂载您的项目至容器内路径,参考命令如下: ```shell -docker run -d -p 8888:8888 -e OPENAI_API_KEY=XXX -v ./sample_standard_app/:/usr/local/etc/workspace/project/sample_standard_app registry.cn-hangzhou.aliyuncs.com/agent_universe/agent_universe:0.0.9_centos8 +docker run -d -p 8888:8888 -e OPENAI_API_KEY=XXX -v ./sample_standard_app/:/usr/local/etc/workspace/project/sample_standard_app registry.cn-hangzhou.aliyuncs.com/agent_universe/agent_universe:0.0.14b1_centos8 ``` 其中`-p 8888:8888`为Web Server的端口映射,前面的8888表示容器内的webserver启动在8888端口,后者表示映射到宿主机的8888端口,可自行根据实际应用的启动情况调整。-e OPENAI_API_KEY=XXX可以添加容器内的环境变量。 `-v {local_dir}:/usr/local/etc/workspace/project/{local_dir_name}`表示把本地`local_dir`目录挂载至容器内的`/usr/local/etc/workspace/project`目录,容器内目录为固定值,不可修改,`local_dir_name`表示你本地文件夹的名字,也就是`local_dir`的最后一级目录名称。 @@ -35,7 +35,7 @@ docker run -d -p 8888:8888 -e OPENAI_API_KEY=XXX -v ./sample_standard_app/:/usr/ ## 方法二:从Github拉取项目 镜像中已安装git命令,您可以通过修改镜像的entrypoint,git clone您的项目后将整个工程复制到指定路径,示例命令: ```shell -docker run -d -p 8888:8888 --entrypoint=/bin/bash registry.cn-hangzhou.aliyuncs.com/agent_universe/agent_universe:0.0.5_centos8_beta -c "git clone {repo_addr}; mv {project_dir} /usr/local/etc/workspace/project; /bin/bash --login /usr/local/etc/workspace/shell/start.sh" +docker run -d -p 8888:8888 --entrypoint=/bin/bash registry.cn-hangzhou.aliyuncs.com/agent_universe/agent_universe:0.0.14b1_centos8 -c "git clone {repo_addr}; mv {project_dir} /usr/local/etc/workspace/project; /bin/bash --login /usr/local/etc/workspace/shell/start.sh" ```` 其中`repo_addr`是你的git项目地址,`project_dir`是工程路径,比如`sample_standard_app`在你git项目下的`project`目录,那么`project_dir`就是`project/sample_standard_app`。 diff --git a/examples/sample_apps/data_agent_app/intelligence/agentic/memory/memory_storage/chroma_memory_storage.yaml b/examples/sample_apps/data_agent_app/intelligence/agentic/memory/memory_storage/chroma_memory_storage.yaml index bc4d0877..d279aed6 100644 --- a/examples/sample_apps/data_agent_app/intelligence/agentic/memory/memory_storage/chroma_memory_storage.yaml +++ b/examples/sample_apps/data_agent_app/intelligence/agentic/memory/memory_storage/chroma_memory_storage.yaml @@ -1,7 +1,7 @@ name: 'chroma_memory_storage' description: 'demo chroma memory storage' collection_name: 'memory' -persist_path: '../../db/memory.db' +persist_path: '../../intelligence/db/memory.db' embedding_model: 'dashscope_embedding' metadata: type: 'MEMORY_STORAGE' diff --git a/examples/sample_standard_app/config/config.toml b/examples/sample_standard_app/config/config.toml index 5db144e4..9def238d 100644 --- a/examples/sample_standard_app/config/config.toml +++ b/examples/sample_standard_app/config/config.toml @@ -37,6 +37,8 @@ query_paraphraser = ['sample_standard_app.intelligence.agentic.knowledge.query_p memory_compressor = ['sample_standard_app.intelligence.agentic.memory.memory_compressor'] # Scan and register memory_storage components for all paths under this list, with priority over the default. memory_storage = ['sample_standard_app.intelligence.agentic.memory.memory_storage'] +# Scan and register log_sink components for all paths under this list, with priority over the default. +log_sink = ['sample_standard_app.intelligence.utils.log_sink'] [SUB_CONFIG_PATH] # Log config file path, an absolute path or a relative path based on the dir where the current config file is located. diff --git a/examples/sample_standard_app/intelligence/agentic/tool/samples/duckduckgo_search.yaml b/examples/sample_standard_app/intelligence/agentic/tool/samples/duckduckgo_search.yaml index 3afff000..32f298d4 100644 --- a/examples/sample_standard_app/intelligence/agentic/tool/samples/duckduckgo_search.yaml +++ b/examples/sample_standard_app/intelligence/agentic/tool/samples/duckduckgo_search.yaml @@ -3,7 +3,7 @@ description: '' tool_type: 'api' input_keys: ['input'] langchain: - module: langchain.tools + module: langchain_community.tools class_name: DuckDuckGoSearchResults init_params: backend: news diff --git a/examples/sample_standard_app/intelligence/utils/log_sink/__init__.py b/examples/sample_standard_app/intelligence/utils/log_sink/__init__.py new file mode 100644 index 00000000..2077047e --- /dev/null +++ b/examples/sample_standard_app/intelligence/utils/log_sink/__init__.py @@ -0,0 +1,7 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/12/10 16:45 +# @Author : fanen.lhy +# @Email : fanen.lhy@antgroup.com +# @FileName: __init__.py.py diff --git a/examples/sample_standard_app/intelligence/utils/log_sink/custom_flask_request_sink.py b/examples/sample_standard_app/intelligence/utils/log_sink/custom_flask_request_sink.py new file mode 100644 index 00000000..317bbfc8 --- /dev/null +++ b/examples/sample_standard_app/intelligence/utils/log_sink/custom_flask_request_sink.py @@ -0,0 +1,22 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/12/10 17:07 +# @Author : fanen.lhy +# @Email : fanen.lhy@antgroup.com +# @FileName: custom_flask_request_sink.py +from agentuniverse.base.util.logging.log_sink.flask_request_log_sink import \ + FlaskRequestLogSink + + +class CustomFlaskRequestSink(FlaskRequestLogSink): + def generate_log(self, flask_request) -> str: + log_string = (f"Request: {flask_request.method} {flask_request.path} " + f"Headers: {dict(flask_request.headers)}") + if flask_request.data: + try: + log_string += f" Body: {flask_request.get_data(as_text=True)}" + except Exception as e: + pass + + return log_string diff --git a/examples/sample_standard_app/intelligence/utils/log_sink/custom_flask_request_sink.yaml b/examples/sample_standard_app/intelligence/utils/log_sink/custom_flask_request_sink.yaml new file mode 100644 index 00000000..131b6fb0 --- /dev/null +++ b/examples/sample_standard_app/intelligence/utils/log_sink/custom_flask_request_sink.yaml @@ -0,0 +1,7 @@ +name: "custom_flask_request_sink" +description: "custom_flask_request_sink" +file_prefix: 'flask_request' +metadata: + type: 'LOG_SINK' + module: 'sample_standard_app.intelligence.utils.log_sink.custom_flask_request_sink' + class: 'CustomFlaskRequestSink' \ No newline at end of file diff --git a/examples/sample_standard_app/intelligence/utils/log_sink/custom_flask_response_sink.py b/examples/sample_standard_app/intelligence/utils/log_sink/custom_flask_response_sink.py new file mode 100644 index 00000000..c3e7006b --- /dev/null +++ b/examples/sample_standard_app/intelligence/utils/log_sink/custom_flask_response_sink.py @@ -0,0 +1,27 @@ + +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/12/10 17:07 +# @Author : fanen.lhy +# @Email : fanen.lhy@antgroup.com +# @FileName: custom_flask_request_sink.py +from agentuniverse.base.util.logging.log_sink.flask_response_log_sink import FlaskResponseLogSink + + +class CustomFlaskResponseSink(FlaskResponseLogSink): + def generate_log(self, flask_response, elapsed_time) -> str: + if isinstance(flask_response, str): + response_str = (f"Response: {flask_response} " + f"Duration: {elapsed_time:.3f}s") + else: + response_str = (f"Response: {flask_response.status_code} {flask_response.content_type} " + f"Duration: {elapsed_time:.3f}s") + + + if flask_response.data: # 记录响应体 + try: + response_str += f' Data:{flask_response.get_data(as_text=True)}' + except Exception as e: + pass + return response_str \ No newline at end of file diff --git a/examples/sample_standard_app/intelligence/utils/log_sink/custom_flask_response_sink.yaml b/examples/sample_standard_app/intelligence/utils/log_sink/custom_flask_response_sink.yaml new file mode 100644 index 00000000..29a47e04 --- /dev/null +++ b/examples/sample_standard_app/intelligence/utils/log_sink/custom_flask_response_sink.yaml @@ -0,0 +1,7 @@ +name: "custom_flask_response_sink" +description: "custom_flask_response_sink" +file_prefix: 'flask_response' +metadata: + type: 'LOG_SINK' + module: 'sample_standard_app.intelligence.utils.log_sink.custom_flask_response_sink' + class: 'CustomFlaskResponseSink' \ No newline at end of file diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/config/config.toml b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/config/config.toml index 41129c09..e3b02456 100644 --- a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/config/config.toml +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/config/config.toml @@ -63,3 +63,17 @@ server_port = 50051 [MONITOR] activate = false dir = './monitor' + + +[CONVERSATION_MEMORY] +# Whether to activate conversation memory. +activate = true +# The name of the memory. +instance_name = 'global_conversation_memory' +# Whether to enable logging. +logging = true +# The language of the memory cn/en. +conversation_format = 'cn' +# The types you want to collection +collection_types = ['llm','tool','agent','knowledge'] + diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/insurance_agent.py b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/insurance_agent.py index c9bba5b7..8267ca96 100644 --- a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/insurance_agent.py +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/insurance_agent.py @@ -10,7 +10,7 @@ from agentuniverse.agent.agent import Agent from agentuniverse.agent.input_object import InputObject from agentuniverse.agent.memory.memory import Memory -from agentuniverse.base.util.agent_util import assemble_memory_input, assemble_memory_output +from agentuniverse.agent.template.agent_template import AgentTemplate from agentuniverse.base.util.prompt_util import process_llm_token from agentuniverse.llm.llm import LLM from agentuniverse.prompt.prompt import Prompt @@ -54,14 +54,12 @@ def execute(self, input_object: InputObject, agent_input: dict, **kwargs) -> dic prompt: Prompt = self.process_prompt(agent_input, **kwargs) process_llm_token(llm, prompt.as_langchain(), self.agent_model.profile, agent_input) # 5. assemble the memory input. - assemble_memory_input(memory, agent_input) + self.load_memory(memory, agent_input) # 6. invoke agent. chain = prompt.as_langchain() | llm.as_langchain_runnable( self.agent_model.llm_params()) | StrOutputParser() res = self.invoke_chain(chain, agent_input, input_object, **kwargs) - # 7. assemble the memory output. - assemble_memory_output(memory=memory, - agent_input=agent_input, - content=f"Human: {agent_input.get('input')}, AI: {res}") + # 7. add the memory output. + self.add_memory(memory, f"Human: {agent_input.get('input')}, AI: {res}", type='', agent_input=agent_input) # 8. return result. return {**agent_input, 'output': res} diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/insurance_agent.yaml b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/insurance_agent.yaml index 2e57e58a..27b1ff50 100644 --- a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/insurance_agent.yaml +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/insurance_agent.yaml @@ -26,7 +26,7 @@ profile: action: tool: memory: - name: 'demo_memory' + conversation_memory: 'global_conversation_memory' metadata: type: 'AGENT' module: 'demo_startup_app_with_single_agent_and_memory.intelligence.agentic.agent.agent_instance.insurance_agent' diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/memory_summarize_agent.yaml b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/memory_summarize_agent.yaml new file mode 100644 index 00000000..a60bc53b --- /dev/null +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/memory_summarize_agent.yaml @@ -0,0 +1,15 @@ +info: + name: 'summarize_agent' + description: 'memory summarize agent' +profile: + prompt_version: 'memory_summarize_cn_prompt' + llm_model: + name: 'qwen_llm' + model_name: 'qwen2.5-72b-instruct' + temperature: 0.7 +memory: + auto_trace: false +metadata: + type: 'AGENT' + module: 'agentuniverse.agent.template.default_summarize_agent_template' + class: 'SummarizeRagAgentTemplate' \ No newline at end of file diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/question_agent_case.yaml b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/question_agent_case.yaml new file mode 100644 index 00000000..ab671be5 --- /dev/null +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/question_agent_case.yaml @@ -0,0 +1,53 @@ +info: + name: 'question_agent_case' + description: 'rag agent case with conversation memory' +profile: + introduction: 你是智能体聊天助手。 + target: 你的任务是基于我们之前的对话和我提供的新信息来帮助用户解决问题。记住,我们要确保对话是自然流畅的,并且尽可能地准确和有用。 + instruction: | + 你是一个智能助手,用户向你提出了一个问题。你的任务是根据用户的问题和之前的聊天记录判断是否需要拆解成多个可以用 Google 搜索的问题。如果需要拆解,则返回一个包含拆解后问题的列表;如果不需要拆解,直接返回是否需要谷歌搜索的布尔值。 + 请按以下步骤进行: + 1. 理解用户的问题:仔细阅读用户的提问,并结合用户之前的聊天记录来理解其需求。 + 2. 判断是否需要谷歌搜索:如果用户的问题很具体且能够直接回答,则返回 false,否则返回 true。 + 3. 拆解问题:如果需要拆解,将问题拆解为不超过三个可以在 Google 上搜索的具体问题。 + 4. 返回格式:根据需要拆解的情况,返回一个 JSON 格式,其中: + need_google_search: 布尔值,表示是否需要拆解并通过 Google 搜索解决(true 或 false)。 + search_questions: 如果 need_google_search 为 true,则返回一个包含拆解后问题的列表;如果为 false,则不返回此字段。 + 5. 之前聊天记录中已有的内容,请不要重复搜索。 + 6. 根据用户的意图,给出合适的要检索的问题。 + + 用户当前的问题:{input} + + ====================================== + + 之前的聊天记录: + {chat_history} + + ====================================== + 例如针对问题:如何使用 requests 库在 Python 中发起 POST 请求? + 输出格式: + {{ + "thought": "给出这个答案的原因", + "need_google_search": true, + "search_questions": [ + "如何在 Python 中使用 requests 库发起 POST 请求?", + "requests 库中的 POST 请求与 GET 请求有何区别?", + "如何使用 Python 的 requests 库发送 JSON 数据?", + "Python 中 requests 库的常见错误和解决方法?", + "Python 请求 POST 方法的最佳实践是什么?" + ] + }} + prompt_version: question_parse_agent.cn + llm_model: + name: 'qwen_llm' + model_name: 'qwen2.5-72b-instruct' + temperature: 0.1 +memory: + conversation_memory: 'global_conversation_memory' + agent_id: rag_agent_case + auto_trace: False + +metadata: + type: 'AGENT' + module: 'agentuniverse.agent.template.rag_agent_template' + class: 'RagAgentTemplate' \ No newline at end of file diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/rag_agent_case.yaml b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/rag_agent_case.yaml new file mode 100644 index 00000000..0895ee7c --- /dev/null +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/rag_agent_case.yaml @@ -0,0 +1,34 @@ +info: + name: 'rag_agent_case' + description: 'rag agent case with conversation memory' +profile: + introduction: 你是智能体聊天助手。 + target: 你的任务是基于我们之前的对话和我提供的新信息来帮助用户解决问题。记住,我们要确保对话是自然流畅的,并且尽可能地准确和有用。 + instruction: | + 背景知识: + {background} + + 之前的对话: + {chat_history} + + ------------------------------------- + 现在请根据以上所有信息,请给出一个既符合对话上下文又利用了最新背景知识的答案。如果你不确定某些细节,坦诚告知用户你不知道,并尝试提供一个一般性的指导或者建议进一步查询的方向。 + 需要回答的问题是: {input} + llm_model: + name: 'qwen_llm' + model_name: 'qwen2.5-72b-instruct' + temperature: 0.1 +action: + tool: + - 'google_search_tool' + knowledge: +memory: + conversation_memory: 'global_conversation_memory' + input_field: 'input' + output_field: 'output' + top_k: 10 + +metadata: + type: 'AGENT' + module: 'demo_startup_app_with_single_agent_and_memory.intelligence.agentic.agent.agent_instance.rag_agent_case_template' + class: 'RagAgentCaseTemplate' \ No newline at end of file diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/rag_agent_case_template.py b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/rag_agent_case_template.py new file mode 100644 index 00000000..711292b6 --- /dev/null +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/agent/agent_instance/rag_agent_case_template.py @@ -0,0 +1,69 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/12/17 14:26 +# @Author : weizjajj +# @Email : weizhongjie.wzj@antgroup.com +# @FileName: rag_agent_case_template.py + +from langchain_core.output_parsers import StrOutputParser +from langchain_core.utils.json import parse_json_markdown + +from agentuniverse.agent.action.tool.tool_manager import ToolManager +from agentuniverse.agent.agent_manager import AgentManager +from agentuniverse.agent.input_object import InputObject +from agentuniverse.agent.memory.memory import Memory +from agentuniverse.agent.template.agent_template import AgentTemplate +from agentuniverse.agent.template.rag_agent_template import RagAgentTemplate +from agentuniverse.base.util.prompt_util import process_llm_token +from agentuniverse.llm.llm import LLM +from agentuniverse.prompt.prompt import Prompt + + +class RagAgentCaseTemplate(RagAgentTemplate): + def execute_query(self, input: str): + """ + Args: + input: user query + """ + # 1. do question analyze + agent_instance: AgentTemplate = AgentManager().get_instance_obj('question_agent_case') + output_object = agent_instance.run(input=input) + output = output_object.get_data('output') + query_info = parse_json_markdown(output) + if not query_info.get('need_google_search'): + return "no_question" + # 2. do google search + questions = query_info.get('search_questions') + tool = ToolManager().get_instance_obj('google_search_tool') + background = [] + for question in questions: + background.append(tool.run(input=question)) + return "Google Search Result: \n" + "\n\n".join(background) + + def customized_execute(self, input_object: InputObject, agent_input: dict, memory: Memory, llm: LLM, prompt: Prompt, + **kwargs) -> dict: + # invoke tool + knowledge_res: str = self.execute_query(agent_input.get('input')) + agent_input['background'] = knowledge_res + + # 1. load memory + self.load_memory(memory, agent_input) + # 2. add user query memory + self.add_memory(memory, f"{agent_input.get('input')}", type='human', agent_input=agent_input) + # 3. load summarize memory + summarize_memory = self.load_summarize_memory(memory, agent_input) + agent_input['background'] = (agent_input['background'] + + f"\nsummarize_memory:\n {summarize_memory}") + process_llm_token(llm, prompt.as_langchain(), self.agent_model.profile, agent_input) + # 4. invoke chain + chain = prompt.as_langchain() | llm.as_langchain_runnable( + self.agent_model.llm_params()) | StrOutputParser() + res = self.invoke_chain(chain, agent_input, input_object, **kwargs) + # 5. stream output + self.add_output_stream(input_object.get_data('output_stream'), res) + # 6. add answer memory + self.add_memory(memory, f"{res}", agent_input=agent_input, type='ai') + # 7. do memory summarize + self.summarize_memory(agent_input, memory) + return {**agent_input, 'output': res} diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/memory/global_conversation_memory.yaml b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/memory/global_conversation_memory.yaml new file mode 100644 index 00000000..0ba3c070 --- /dev/null +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/memory/global_conversation_memory.yaml @@ -0,0 +1,13 @@ +name: 'global_conversation_memory' +description: 'global memory with local storage' +type: 'short_term' +memory_key: 'chat_history' +max_tokens: 3000 +memory_compressor: demo_memory_compressor +memory_storages: + - sqlite_conversation_memory_storage +memory_summarize_agent: summarize_agent +metadata: + type: 'MEMORY' + module: 'agentuniverse.agent.memory.memory' + class: 'Memory' \ No newline at end of file diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/memory/memory_storage/sqlite_conversation_memory_storage.yaml b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/memory/memory_storage/sqlite_conversation_memory_storage.yaml new file mode 100644 index 00000000..c6e48034 --- /dev/null +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/memory/memory_storage/sqlite_conversation_memory_storage.yaml @@ -0,0 +1,8 @@ +name: 'sqlite_conversation_memory_storage' +description: 'demo sqlite memory storage' +sqldb_path: 'sqlite:///../../intelligence/db/sqlite_memory.db' +sql_table_name: 'conversation_memory' +metadata: + type: 'MEMORY_STORAGE' + module: 'agentuniverse.agent.memory.conversation_memory.memory_storage.sqlite_conversation_memory_storage' + class: 'SqliteMemoryStorage' \ No newline at end of file diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/tool/google_search_tool.py b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/tool/google_search_tool.py new file mode 100644 index 00000000..41117e1f --- /dev/null +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/tool/google_search_tool.py @@ -0,0 +1,31 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- +# @Time : 2024/3/31 11:00 +# @Author : wangchongshi +# @Email : wangchongshi.wcs@antgroup.com +# @FileName: google_search_tool.py +from typing import Optional + +from pydantic import Field +from langchain_community.utilities.google_serper import GoogleSerperAPIWrapper +from agentuniverse.agent.action.tool.tool import Tool, ToolInput +from agentuniverse.base.util.env_util import get_from_env + + + +class GoogleSearchTool(Tool): + """The demo google search tool. + + Implement the execute method of demo google search tool, using the `GoogleSerperAPIWrapper` to implement a simple Google search. + + Note: + You need to sign up for a free account at https://serper.dev and get the serpher api key (2500 free queries). + """ + + serper_api_key: Optional[str] = Field(default_factory=lambda: get_from_env("SERPER_API_KEY")) + + def execute(self, tool_input: ToolInput): + input = tool_input.get_data("input") + # get top10 results from Google search. + search = GoogleSerperAPIWrapper(serper_api_key=self.serper_api_key, k=10, gl="us", hl="en", type="search") + return search.run(query=input) \ No newline at end of file diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/tool/google_search_tool.yaml b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/tool/google_search_tool.yaml new file mode 100644 index 00000000..4461a6be --- /dev/null +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/agentic/tool/google_search_tool.yaml @@ -0,0 +1,12 @@ +name: 'google_search_tool' +description: | + 该工具可以用来进行谷歌搜索,工具的输入是你想搜索的内容。 + 工具输入示例: + 示例1: 你想要搜索上海的天气时,工具的输入应该是:上海今天的天气 + 示例2: 你想要搜索日本的天气时,工具的输入应该是:日本的天气 +tool_type: 'api' +input_keys: [ 'input' ] +metadata: + type: 'TOOL' + module: 'demo_startup_app_with_single_agent_and_memory.intelligence.agentic.tool.google_search_tool' + class: 'GoogleSearchTool' \ No newline at end of file diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/test/insurance_agent_test.py b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/test/insurance_agent_test.py index 98b8d141..a855fc3c 100644 --- a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/test/insurance_agent_test.py +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/test/insurance_agent_test.py @@ -11,6 +11,8 @@ from agentuniverse.agent.agent_manager import AgentManager import uuid +from agentuniverse.base.context.framework_context_manager import FrameworkContextManager + AgentUniverse().start(config_path='../../config/config.toml', core_mode=True) @@ -22,5 +24,8 @@ def chat(question: str, session_id: str): if __name__ == '__main__': s_id = str(uuid.uuid4()) + FrameworkContextManager().set_context('session_id', s_id) + FrameworkContextManager().set_context('trace_id', str(uuid.uuid4())) chat("保险产品A怎么续保", s_id) + FrameworkContextManager().set_context('trace_id', uuid.uuid4().hex) chat("我刚才问了什么问题", s_id) diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/test/memory_query_test.py b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/test/memory_query_test.py new file mode 100644 index 00000000..91c107de --- /dev/null +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/test/memory_query_test.py @@ -0,0 +1,31 @@ +from typing import List + +from agentuniverse.agent.memory.memory import Memory +from agentuniverse.agent.memory.memory_manager import MemoryManager +from agentuniverse.agent.memory.message import Message +from agentuniverse.base.agentuniverse import AgentUniverse + +AgentUniverse().start(config_path='../../config/config.toml', core_mode=True) + + +def test_query_memory(s_id: str): + memory_instance: Memory = MemoryManager().get_instance_obj('global_conversation_memory') + messages: List[Message] = memory_instance.get(session_id=s_id, top_k=500, type=['input', 'output']) + for message in messages: + # print(message.trace_id) + print(f"{message.metadata.get('timestamp')} {message.metadata.get('prefix')}: {message.content}") + +def test_query_memory_with_trace_id(s_id: str): + memory_instance: Memory = MemoryManager().get_instance_obj('global_conversation_memory') + messages: List[Message] = memory_instance.get(session_id=s_id, trace_id="39fbcb33aca8427cb59c37d6f39adc10", + type=['input', 'output']) + for message in messages: + # print(message.trace_id) + print(f"{message.metadata.get('timestamp')} {message.metadata.get('prefix')}: {message.content}") + + + +if __name__ == '__main__': + s_id = "d369ff14-1ad4-4192-b624-18c8cf1a0c11" + # test_query_memory(s_id) + test_query_memory_with_trace_id(s_id) diff --git a/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/test/rag_agent_case_test.py b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/test/rag_agent_case_test.py new file mode 100644 index 00000000..26095eb6 --- /dev/null +++ b/examples/startup_app/demo_startup_app_with_single_agent_and_memory/intelligence/test/rag_agent_case_test.py @@ -0,0 +1,40 @@ +# !/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# @Time : 2024/12/30 15:53 +# @Author : weizjajj +# @Email : weizhongjie.wzj@antgroup.com +# @FileName: rag_agent_case_test.py + +import time +from agentuniverse.agent.output_object import OutputObject +from agentuniverse.base.agentuniverse import AgentUniverse +from agentuniverse.agent.agent import Agent +from agentuniverse.agent.agent_manager import AgentManager +import uuid + +from agentuniverse.base.context.framework_context_manager import FrameworkContextManager + +AgentUniverse().start(config_path='../../config/config.toml', core_mode=True) + + +def chat(question: str, session_id: str): + FrameworkContextManager().set_context('trace_id',uuid.uuid4().hex) + instance: Agent = AgentManager().get_instance_obj('rag_agent_case') + output_object: OutputObject = instance.run(input=question, session_id=session_id) + # print(output_object.get_data('output') + '\n') + + +if __name__ == '__main__': + s_id = "d369ff14-1ad4-4192-b624-18c8cf1a0c11" + FrameworkContextManager().set_context('session_id', s_id) + chat("我想去东京旅游,你知道东京天气预报么", s_id) + chat("这种天气我应该穿什么衣服", s_id) + chat("有什么推荐的景点么", s_id) + chat("可以帮我规划一个三天的旅行路线么", s_id) + chat("我从上海出发,乘坐什么交通工具比较合适", s_id) + chat("我不太喜欢坐高铁", s_id) + chat("我不想坐飞机", s_id) + time.sleep(5) + # FrameworkContextManager().set_context('trace_id', uuid.uuid4().hex) + # chat("我刚才问了什么问题", s_id)