From 59ccc086aa31ef1611a5f83245b6ba31b73e6f56 Mon Sep 17 00:00:00 2001 From: "taoquanyu.tqy" Date: Mon, 1 Dec 2025 16:18:53 +0800 Subject: [PATCH 1/4] feat(memory): add profile schema and user profile management - Add CreateProfileSchema component to create user profile schemas - Add GetUserProfile component to retrieve user profiles by schema and user ID - Update memory service endpoints for profile-related operations - Refactor delete memory operation to use DELETE HTTP method - Change list memory operation to use GET HTTP method with query params - Add model validators and improve input/output data models - Update URL patterns for memory node operations - Introduce new data models for profile attributes and user profiles --- .../components/memory/modelstudio_memory.py | 239 +++++++++++++++--- 1 file changed, 208 insertions(+), 31 deletions(-) diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory.py b/src/agentscope_bricks/components/memory/modelstudio_memory.py index d300911..272bfae 100644 --- a/src/agentscope_bricks/components/memory/modelstudio_memory.py +++ b/src/agentscope_bricks/components/memory/modelstudio_memory.py @@ -3,19 +3,18 @@ from typing import List, Dict, Any, Optional import aiohttp -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator from agentscope_bricks.base.component import Component # ENV-PRE -MEMORY_SERVICE_ENDPOINT = os.getenv( - "MEMORY_SERVICE_ENDPOINT", - "https://dashscope.aliyuncs.com/api/v2/apps/memory", -) -ADD_MEMORY_URL = f"{MEMORY_SERVICE_ENDPOINT}/add" -SEARCH_MEMORY_URL = f"{MEMORY_SERVICE_ENDPOINT}/search" -LIST_MEMORY_URL = f"{MEMORY_SERVICE_ENDPOINT}/list" -DELETE_MEMORY_URL = f"{MEMORY_SERVICE_ENDPOINT}/delete" +MEMORY_SERVICE_ENDPOINT = os.getenv("MEMORY_SERVICE_ENDPOINT", "https://dashscope.aliyuncs.com/api/v2/apps/memory") +ADD_MEMORY_URL = f'{MEMORY_SERVICE_ENDPOINT}/add' +SEARCH_MEMORY_URL = f'{MEMORY_SERVICE_ENDPOINT}/memory_nodes/search' +LIST_MEMORY_URL = f'{MEMORY_SERVICE_ENDPOINT}/memory_nodes' +DELETE_MEMORY_URL_PATTERN = f'{MEMORY_SERVICE_ENDPOINT}/memory_nodes/{{memory_node_id}}' +CREATE_PROFILE_SCHEMA_URL = f"{MEMORY_SERVICE_ENDPOINT}/profile_schemas" +GET_USER_PROFILE_URL_PATTERN = f"{MEMORY_SERVICE_ENDPOINT}/profile_schemas/{{schema_id}}/user_profile" class Message(BaseModel): @@ -287,16 +286,16 @@ async def _arun( # included payload = args.model_dump(exclude_none=True) - # Send request async with aiohttp.ClientSession() as session: - async with session.post( - self.list_memory_url, - json=payload, - headers={ - "Content-Type": "application/json", - "User-Agent": "agentscope-bricks", - "Authorization": f"Bearer {self.api_key}", - }, + async with session.get( + self.list_memory_url, + params=payload, + json={}, + headers={ + "Content-Type": "application/json", + "User-Agent": "agentscope-bricks", + "Authorization": f"Bearer {self.api_key}", + }, ) as response: if response.status != 200: error_text = await response.text() @@ -332,7 +331,7 @@ class DeleteMemory(Component[DeleteMemoryInput, DeleteMemoryOutput]): def __init__(self) -> None: super().__init__() self.service_id = os.getenv("MODELSTUDIO_SERVICE_ID", "memory_service") - self.delete_memory_url = DELETE_MEMORY_URL + self.delete_memory_url_pattern = DELETE_MEMORY_URL_PATTERN self.api_key = os.getenv("DASHSCOPE_API_KEY") if not self.api_key: raise ValueError( @@ -355,20 +354,18 @@ async def _arun( DeleteMemoryOutput: Delete memory output """ try: - # Build request body - all fields including extra fields will be - # included - payload = args.model_dump(exclude_none=True) + # Build URL with path param and include body as per example + url = self.delete_memory_url_pattern.format(memory_node_id=args.memory_node_id) - # Send request async with aiohttp.ClientSession() as session: - async with session.post( - self.delete_memory_url, - json=payload, - headers={ - "Content-Type": "application/json", - "User-Agent": "agentscope-bricks", - "Authorization": f"Bearer {self.api_key}", - }, + async with session.delete( + url, + json={}, + headers={ + "Content-Type": "application/json", + "User-Agent": "agentscope-bricks", + "Authorization": f"Bearer {self.api_key}", + }, ) as response: if response.status != 200: error_text = await response.text() @@ -384,3 +381,183 @@ async def _arun( except Exception as e: raise Exception(f"Error in DeleteMemory: {str(e)}") + + +class ProfileAttribute(BaseModel): + name: str = Field(..., description='Attribute name') + description: Optional[str] = Field(None, description='Attribute description') + immutable: Optional[bool] = Field(False, description='Whether the attribute is immutable') + default_value: Optional[Any] = Field(None, description='Default value for the attribute') + + +class CreateProfileSchemaInput(BaseModel): + name: str = Field(..., description='Profile schema name') + description: Optional[str] = Field(None, description='Profile schema description') + attributes: List[ProfileAttribute] = Field(..., description='List of attribute definitions (>=1)') + + @model_validator(mode='after') + def validate_attributes(self) -> 'CreateProfileSchemaInput': + if not self.attributes or len(self.attributes) == 0: + raise ValueError('attributes must contain at least one item') + return self + + class Config: + extra = "allow" + + +class CreateProfileSchemaOutput(BaseModel): + profile_schema_id: str = Field(..., description='Created profile schema id') + request_id: str = Field(..., description='Request id') + + +class CreateProfileSchema(Component[CreateProfileSchemaInput, CreateProfileSchemaOutput]): + """ + Create a user profile schema with attribute definitions. + """ + + name = 'create_profile_schema' + description = 'Create a profile schema with attribute definitions' + + def __init__(self): + super().__init__() + self.service_id = os.getenv("MODELSTUDIO_SERVICE_ID", "profile_service") + self.create_profile_schema_url = CREATE_PROFILE_SCHEMA_URL + self.api_key = os.getenv("DASHSCOPE_API_KEY") + if not self.api_key: + raise ValueError("DASHSCOPE_API_KEY environment variable is required") + + async def _arun( + self, + args: CreateProfileSchemaInput, + **kwargs: Any + ) -> CreateProfileSchemaOutput: + """ + Create a profile schema + + Args: + args: CreateProfileSchemaInput + **kwargs: Additional parameters + + Returns: + CreateProfileSchemaOutput: Result including profile_schema_id and request_id + """ + try: + payload = args.model_dump(exclude_none=True) + + async with aiohttp.ClientSession() as session: + async with session.post( + self.create_profile_schema_url, + json=payload, + headers={ + "Content-Type": "application/json", + "User-Agent": "agentscope-bricks", + "Authorization": f"Bearer {self.api_key}" + } + ) as response: + if response.status != 200: + error_text = await response.text() + raise Exception(f"Create profile schema failed with status {response.status}: {error_text}") + + result = await response.json() + return CreateProfileSchemaOutput( + profile_schema_id=result.get('profile_schema_id', ''), + request_id=result.get('request_id', '') + ) + except Exception as e: + raise Exception(f"Error in CreateProfileSchema: {str(e)}") + + +class UserProfileAttribute(BaseModel): + name: str = Field(..., description='Attribute name') + id: str = Field(..., description='Attribute id') + value: Optional[Any] = Field(None, description='Attribute value') + + +class UserProfile(BaseModel): + schema_description: Optional[str] = Field(None, description='Schema description') + schema_name: Optional[str] = Field(None, description='Schema name') + attributes: List[UserProfileAttribute] = Field(default_factory=list, description='User attributes') + + +class GetUserProfileInput(BaseModel): + schema_id: str = Field(..., description='Profile schema id') + user_id: str = Field(..., description='End user id') + + +class GetUserProfileOutput(BaseModel): + request_id: str = Field(..., description='Request id') + profile: UserProfile = Field(..., description='User profile') + + +class GetUserProfile(Component[GetUserProfileInput, GetUserProfileOutput]): + """ + Get user profile by schema id and user id. + """ + + name = 'get_user_profile' + description = 'Get user profile by schema id and user id' + + def __init__(self): + super().__init__() + self.service_id = os.getenv("MODELSTUDIO_SERVICE_ID", "profile_service") + self.get_user_profile_url_pattern = GET_USER_PROFILE_URL_PATTERN + self.api_key = os.getenv("DASHSCOPE_API_KEY") + if not self.api_key: + raise ValueError("DASHSCOPE_API_KEY environment variable is required") + + async def _arun( + self, + args: GetUserProfileInput, + **kwargs: Any + ) -> GetUserProfileOutput: + """ + Get a user profile + + Args: + args: GetUserProfileInput + **kwargs: Additional parameters + + Returns: + GetUserProfileOutput: Profile and request id + """ + try: + # Build URL without user_id in path; user_id will be a query parameter + url = self.get_user_profile_url_pattern.format(schema_id=args.schema_id) + + async with aiohttp.ClientSession() as session: + async with session.get( + url, + params={"user_id": args.user_id}, + json={}, + headers={ + "Content-Type": "application/json", + "User-Agent": "agentscope-bricks", + "Authorization": f"Bearer {self.api_key}" + } + ) as response: + if response.status != 200: + error_text = await response.text() + raise Exception(f"Get user profile failed with status {response.status}: {error_text}") + + result = await response.json() + profile_raw = result.get('profile', {}) + attributes = [ + UserProfileAttribute( + name=item.get('name', ''), + id=item.get('id', ''), + value=item.get('value') + ) for item in profile_raw.get('attributes', []) + ] + + profile = UserProfile( + schema_description=profile_raw.get('schemaDescription'), + schema_name=profile_raw.get('schemaName'), + attributes=attributes + ) + + return GetUserProfileOutput( + request_id=result.get('requestId', ''), + profile=profile + ) + except Exception as e: + raise Exception(f"Error in GetUserProfile: {str(e)}") From c1d1da78a993e963117b8f9485ec52dbe2de19db Mon Sep 17 00:00:00 2001 From: "taoquanyu.tqy" Date: Mon, 1 Dec 2025 19:53:16 +0800 Subject: [PATCH 2/4] docs(memory): update memory demo script with placeholder API key - Replace actual DashScope API key with placeholder value - Maintain existing environment variable structure - Preserve all other configuration settings unchanged docs(examples): remove memory_with_llm.py from example files - Removed memory_with_llm.py from components README documentation - Removed memory_with_llm.py execution command from example instructions feat(memory): enhance error handling and memory node parsing - Add MemoryValidationError exception for 400 status codes - Improve error response parsing with JSON/text fallback - Extract and log detailed error information (code, message, request ID) - Handle both list and dict formats for memory nodes in API responses - Add comprehensive error handling in demo examples - Update exception classes with optional error code and request ID fields - Format error messages with all available error details - Add debug logging for API response structures - Wrap single memory nodes in lists for consistent processing - Improve exception re-raising with proper error information preservation feat(memory): enhance user profile extraction and display in memory demo - Improved explanation of automatic memory and profile extraction - Enhanced dialogue display with highlighting of profile-related content - Added visual formatting for user profile and schema information - Included waiting logic to ensure profile extraction completion - Updated demo flow to showcase extracted user profile features feat(memory): add memory demo with profile and LLM integration - Created a comprehensive memory demo showcasing core functionalities - Implemented user profile schema creation and management - Added example dialogue processing with automatic memory extraction - Integrated memory listing with pagination support - Enabled semantic memory search combined with LLM response generation - Included user profile retrieval based on custom schemas - Provided memory cleanup and resource management utilities - Added bash script for easy demo execution and environment setup - Fixed asyncio sleep calls in existing memory examples - Enhanced logging configuration and error handling - Improved console output formatting and user experience feat(memory): implement ModelStudio Memory components and services - Added core memory components: AddMemory, SearchMemory, ListMemory, DeleteMemory - Implemented user profile management: CreateProfileSchema, GetUserProfile - Created base class ModelStudioMemoryBase with common API functionality - Added comprehensive error handling with custom exception types - Included configuration management through MemoryServiceConfig - Provided Pydantic models for all input/output data structures - Added support for async operations and session management - Implemented proper HTTP request handling with authentication - Added detailed logging for debugging and monitoring - Organized code into modular structure with clear separation of concerns --- examples/components/README.md | 2 - examples/components/memory_with_llm.py | 6 +- examples/memory/__init__.py | 0 examples/memory/memory_demo.py | 582 +++++++++++++++++ examples/memory/memory_demo.sh | 63 ++ .../components/memory/__init__.py | 14 + .../components/memory/modelstudio_memory.py | 563 ----------------- .../memory/modelstudio_memory/__init__.py | 106 ++++ .../memory/modelstudio_memory/base.py | 215 +++++++ .../memory/modelstudio_memory/config.py | 85 +++ .../memory/modelstudio_memory/core.py | 594 ++++++++++++++++++ .../memory/modelstudio_memory/exceptions.py | 69 ++ .../memory/modelstudio_memory/models.py | 246 ++++++++ 13 files changed, 1977 insertions(+), 568 deletions(-) create mode 100644 examples/memory/__init__.py create mode 100644 examples/memory/memory_demo.py create mode 100644 examples/memory/memory_demo.sh delete mode 100644 src/agentscope_bricks/components/memory/modelstudio_memory.py create mode 100644 src/agentscope_bricks/components/memory/modelstudio_memory/__init__.py create mode 100644 src/agentscope_bricks/components/memory/modelstudio_memory/base.py create mode 100644 src/agentscope_bricks/components/memory/modelstudio_memory/config.py create mode 100644 src/agentscope_bricks/components/memory/modelstudio_memory/core.py create mode 100644 src/agentscope_bricks/components/memory/modelstudio_memory/exceptions.py create mode 100644 src/agentscope_bricks/components/memory/modelstudio_memory/models.py diff --git a/examples/components/README.md b/examples/components/README.md index 03c1734..fdee0bf 100644 --- a/examples/components/README.md +++ b/examples/components/README.md @@ -11,7 +11,6 @@ This examples provides of using various AgentScope-Bricks components in differe - `create_component.py` - Component creation and configuration examples #### Advanced Integration -- `memory_with_llm.py` - Long-term Memory integrated with LLM models - `rag_with_llm.py` - RAG with LLM call - `search_with_llm.py` - Search components with LLM processing @@ -26,7 +25,6 @@ export PYTHONPATH=$PYTHONPATH:/path/to/agentscope-bricks/project 2. Run individual examples: ```shell python llm_service.py # Basic LLM streaming service -python memory_with_llm.py # Memory + LLM integration python rag_with_llm.py # RAG with LLM python search_with_llm.py # Search with LLM ``` diff --git a/examples/components/memory_with_llm.py b/examples/components/memory_with_llm.py index 0c814da..81ac0a8 100644 --- a/examples/components/memory_with_llm.py +++ b/examples/components/memory_with_llm.py @@ -163,7 +163,7 @@ async def main() -> None: add_result = await add_memory_example() print("Add Memory Result:", add_result) - time.sleep(5) + await asyncio.sleep(5) # 2. Delete the newly added memory node print("\n=== Deleting Memory ===") if add_result.memory_nodes: @@ -172,7 +172,7 @@ async def main() -> None: delete_result = await delete_memory_example(memory_node_id) print("Delete Memory Result:", delete_result) - time.sleep(5) + await asyncio.sleep(5) # 3. List memory print("\n=== Listing Memory ===") list_result = await list_memory_example() @@ -182,7 +182,7 @@ async def main() -> None: print(f"Memory Node ID: {node.memory_node_id}") print(f"Memory Node Content: {node.content}") - time.sleep(5) + await asyncio.sleep(5) # 4. Search memory user_query = "明天需要提醒我什么事?" print("\n=== Searching Memory ===") diff --git a/examples/memory/__init__.py b/examples/memory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/memory/memory_demo.py b/examples/memory/memory_demo.py new file mode 100644 index 0000000..d24d55c --- /dev/null +++ b/examples/memory/memory_demo.py @@ -0,0 +1,582 @@ +import asyncio +import logging +import os +import sys +import time +from typing import List, Tuple + +from agentscope_bricks.components.memory.modelstudio_memory import ( + AddMemory, + SearchMemory, + ListMemory, + DeleteMemory, + CreateProfileSchema, + GetUserProfile, + GetUserProfileInput, + Message, + AddMemoryInput, + SearchMemoryInput, + ListMemoryInput, + DeleteMemoryInput, + CreateProfileSchemaInput, + ProfileAttribute, + MemoryAPIError, + MemoryAuthenticationError, + MemoryNotFoundError, + MemoryValidationError, +) +from agentscope_bricks.models.llm import BaseLLM +from agentscope_bricks.utils.schemas.oai_llm import Parameters + +# ===== 配置日志,过滤掉冗长的调试信息 ===== +# 从环境变量读取日志级别,默认为 WARNING +LOG_LEVEL = os.getenv('LOG_LEVEL', 'WARNING').upper() +logging.basicConfig( + level=getattr(logging, LOG_LEVEL, logging.WARNING), + format='%(levelname)s: %(message)s' if LOG_LEVEL == 'WARNING' else '%(asctime)s [%(levelname)s] %(name)s: %(message)s' +) + +# 特别禁用某些组件的详细日志(除非明确设置为 DEBUG) +if LOG_LEVEL != 'DEBUG': + logging.getLogger('agentscope_bricks').setLevel(logging.WARNING) + logging.getLogger('httpx').setLevel(logging.WARNING) + logging.getLogger('httpcore').setLevel(logging.WARNING) + + +def require_env(name: str) -> str: + value = os.getenv(name) + if not value: + print(f"[ERROR] Required environment variable not set: {name}", file=sys.stderr) + sys.exit(1) + return value + + +def get_env(name: str, default: str) -> str: + value = os.getenv(name, default) + return value + + +def truncate(text: str, length: int = 120) -> str: + if text is None: + return "" + if len(text) <= length: + return text + return text[: length - 3] + "..." + + +def print_section(title: str) -> None: + bar = "=" * 70 + print(f"\n{bar}\n{title}\n{bar}") + + +def print_info(message: str) -> None: + print(f"[system_info] {message}") + + +def print_warn(message: str) -> None: + print(f"[warn] {message}") + + +def print_success(message: str) -> None: + print(f"[success] {message}") + + +def print_error(message: str) -> None: + print(f"[ERROR] {message}") + + +def format_api_error(error: MemoryAPIError) -> str: + """格式化 API 错误信息以便显示""" + parts = [] + + # 提取错误消息主体(不包括 __str__ 方法添加的额外信息) + error_message = str(error).split(" | ")[0] + parts.append(f"错误信息: {error_message}") + + if error.error_code: + parts.append(f"错误代码: {error.error_code}") + + if error.status_code: + parts.append(f"HTTP 状态码: {error.status_code}") + + if error.request_id: + parts.append(f"Request ID: {error.request_id}") + + return "\n ".join(parts) + + +async def step_create_profile_schema(create_profile_schema: CreateProfileSchema) -> str: + """创建用户画像 Schema""" + print_info("用户画像 Schema 用于定义用户有哪些字段(如年龄、爱好)。") + print("") + + payload = CreateProfileSchemaInput( + name="用户画像(示例)", + description="用于演示的用户基础画像 Schema", + attributes=[ + ProfileAttribute(name="年龄", description="用户年龄"), + ProfileAttribute(name="爱好", description="兴趣偏好"), + ], + ) + + # 展示示例参数 + print_info("请求参数:") + print_info(f" · Schema 名称:{payload.name}") + print_info(f" · Schema 描述:{payload.description}") + print_info(f" · 字段定义:") + for idx, attr in enumerate(payload.attributes, start=1): + print_info(f" [{idx}] {attr.name} - {attr.description}") + print("") + + result = await create_profile_schema.arun(payload) + print_success(f"✓ 已创建用户画像 Schema") + print_info(f" Schema ID:{result.profile_schema_id}") + print_info(f" 请求ID:{result.request_id}") + print("") + + return result.profile_schema_id + + +def example_messages() -> List[Message]: + return [ + Message(role="user", content="每天上午9点提醒我喝水,下午3点复习笔记。"), + Message(role="assistant", content="好的,我已经记录下来。"), + Message(role="user", + content="还有明天记得提醒我给诺成老师买个生日礼物,诺成老师今年30岁了,比我大三岁。我们的爱好相同,经常一起踢球,所以我打算给诺成老师买一个精美的足球"), + Message(role="assistant", content="好的,我明天会提醒你"), + ] + + +async def step_add_memory(add_memory: AddMemory, end_user_id: str, profile_schema_id: str) -> List[str]: + """添加对话记忆到记忆服务""" + print_info("我们将一段对话提交到记忆服务,服务会自动完成两件事:") + print_info(" 1️⃣ 抽取并保存记忆条目(memory nodes)") + print_info(" 2️⃣ 从对话中提取用户画像信息(年龄、爱好等)") + print("") + + now_ts = int(time.time()) + msgs = example_messages() + payload = AddMemoryInput( + user_id=end_user_id, + messages=msgs, + timestamp=now_ts, + profile_schema=profile_schema_id, + meta_data={ + "location_name": "杭州", + "geo_coordinate": "120.1551,30.2741", + }, + ) + + # 展示示例参数 + print_info("📥 请求参数:") + print_info(f" · 用户ID:{payload.user_id}") + print_info(f" · Profile Schema ID:{truncate(profile_schema_id, 50)}") + print_info(f" · 时间戳:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(payload.timestamp))}") + print_info(f" · 对话消息数:{len(payload.messages)} 条") + print_info(f" · 元数据:位置={payload.meta_data.get('location_name')}, 坐标={payload.meta_data.get('geo_coordinate')}") + print("") + + print_info("💬 对话内容(注意画像信息):") + for idx, m in enumerate(payload.messages, start=1): + role_icon = "👤" if m.role == "user" else "🤖" + content_str = str(m.content) + + # 突出显示包含画像信息的对话 + if "30岁" in content_str or "踢球" in content_str: + print(f" {role_icon} [{m.role}] {truncate(content_str, 100)} 🎯") + else: + print(f" {role_icon} [{m.role}] {truncate(content_str, 100)}") + print("") + print_info(" 🎯 = 包含可提取的画像信息(年龄、爱好)") + print("") + + add_result = await add_memory.arun(payload) + + # 调试:打印返回结果类型 + print_info(f"🔍 调试信息:memory_nodes 类型 = {type(add_result.memory_nodes)}") + + # 兼容处理:如果 memory_nodes 不是列表,转换为列表 + if isinstance(add_result.memory_nodes, list): + memory_nodes_list = add_result.memory_nodes + else: + # 如果是单个对象,包装成列表 + memory_nodes_list = [add_result.memory_nodes] if add_result.memory_nodes else [] + + node_ids = [n.memory_node_id for n in memory_nodes_list if n.memory_node_id] + + if node_ids: + print_success(f"✓ 成功新增 {len(node_ids)} 条记忆条目") + print_info(f" 请求ID:{add_result.request_id}") + print("") + print_info("📝 生成的记忆条目:") + print("") + for idx, node in enumerate(memory_nodes_list, start=1): + print(f" [{idx}] {truncate(node.content, 100)}") + print(f" ID: {node.memory_node_id}") + if idx < len(memory_nodes_list): + print("") + print("") + else: + print_warn("⚠ 未返回任何记忆条目 ID,稍后删除步骤将跳过。") + + return node_ids + + +async def step_list_memory(list_memory: ListMemory, end_user_id: str, page_num: int = 1, page_size: int = 10) -> List[str]: + """列出用户的所有记忆条目(分页)""" + print_info("列出该用户当前保存的所有记忆条目(分页查询)。") + print("") + + payload = ListMemoryInput(user_id=end_user_id, page_num=page_num, page_size=page_size) + + # 展示示例参数 + print_info("请求参数:") + print_info(f" · 用户ID:{payload.user_id}") + print_info(f" · 页码:{payload.page_num}") + print_info(f" · 每页数量:{payload.page_size}") + print("") + + result = await list_memory.arun(payload) + total_pages = (result.total + result.page_size - 1) // result.page_size if result.page_size else 1 + + print_success(f"✓ 列表获取成功 (请求ID: {result.request_id})") + print_info(f"📊 分页信息:第 {result.page_num}/{total_pages} 页,每页 {result.page_size} 条,共 {result.total} 条") + print("") + + if not result.memory_nodes: + print_info("(当前页无记忆条目)") + return [] + + print_info(f"📝 记忆条目列表(当前页共 {len(result.memory_nodes)} 条):") + print("") + + existing_ids = [] + for idx, node in enumerate(result.memory_nodes, start=1): + existing_ids.append(node.memory_node_id or "") + print(f" [{idx}] {truncate(node.content, 100)}") + print(f" ID: {node.memory_node_id}") + if idx < len(result.memory_nodes): + print("") + + print("") + return [nid for nid in existing_ids if nid] + + +async def step_search_memory_with_llm(search_memory: SearchMemory, llm: BaseLLM, end_user_id: str) -> Tuple[List[str], str]: + """检索记忆并使用大模型生成个性化回答""" + user_query = "今天和明天需要提醒我做什么?" + + print_info("我们将用一个自然语言问题来检索相关记忆,然后让大模型基于这些记忆生成个性化回答。") + print("") + + # 1. 检索记忆 + print_info("🔍 第一步:检索相关记忆") + payload = SearchMemoryInput( + user_id=end_user_id, + messages=[Message(role="user", content=user_query)], + top_k=5, + min_score=0, + ) + + print_info("检索参数:") + print_info(f" · 用户ID:{payload.user_id}") + print_info(f" · 用户问题:{user_query}") + print_info(f" · 返回条数:top_k={payload.top_k}") + print_info(f" · 最低分数:min_score={payload.min_score}") + print("") + + search_result = await search_memory.arun(payload) + print_success(f"✓ 检索完成 (请求ID: {search_result.request_id})") + + if not search_result.memory_nodes: + print_warn("未找到相关记忆条目") + return [], user_query + + print_info(f"找到 {len(search_result.memory_nodes)} 条相关记忆:") + print("") + + hit_ids = [] + for idx, node in enumerate(search_result.memory_nodes, start=1): + hit_ids.append(node.memory_node_id or "") + print(f" [{idx}] {truncate(node.content, 100)}") + print(f" ID: {node.memory_node_id}") + + print("") + print("─" * 70) + print("") + + # 2. 使用大模型生成回答 + print_info("🤖 第二步:基于检索到的记忆,让大模型生成个性化回答") + print("") + + context_lines = [f"- {node.content}" for node in search_result.memory_nodes] + system_prompt = ( + "你是一名助理。根据以下检索到的记忆回答用户问题。\n\n" + + "记忆内容:\n" + + ("\n".join(context_lines) if context_lines else "(无检索结果)") + ) + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_query}, + ] + + params = Parameters(stream=True, stream_options={"include_usage": True}) + model_name = "qwen-max" + + print_info(f"模型:{model_name}(流式输出)") + print_info(f"问题:{user_query}") + print("") + print_success("模型回答:") + print("┌" + "─" * 68 + "┐") + print("│", end=" ") + + char_count = 0 + async for chunk in llm.astream(model=model_name, messages=messages, parameters=params): + if chunk.choices: + delta = getattr(chunk.choices[0], "delta", None) + if delta is not None: + text = str(delta) + print(text, end="", flush=True) + char_count += len(text) + # 每70个字符换行并添加边框 + if char_count >= 66: + print(" │") + print("│", end=" ") + char_count = 0 + + if char_count > 0: + # 补齐最后一行 + padding = " " * (67 - char_count) + print(f"{padding}│") + print("└" + "─" * 68 + "┘") + print("") + + return [hid for hid in hit_ids if hid], user_query + + +async def step_get_user_profile(get_user_profile: GetUserProfile, schema_id: str, end_user_id: str) -> None: + """获取并展示用户画像信息""" + print_info("🎯 用户画像功能展示") + print("") + print_info("💡 说明:记忆服务会自动从对话中提取用户信息,填充到画像字段中。") + print_info(" 例如:从 '诺成老师今年30岁,比我大三岁' 可推断出用户27岁") + print_info(" 从 '我们经常一起踢球' 可推断出用户爱好是足球") + print("") + + payload = GetUserProfileInput(schema_id=schema_id, user_id=end_user_id) + + # 展示示例参数 + print_info("📥 请求参数:") + print_info(f" · Schema ID:{truncate(payload.schema_id, 50)}") + print_info(f" · 用户ID:{payload.user_id}") + print("") + + result = await get_user_profile.arun(payload) + print_success(f"✓ 已获取用户画像 (请求ID: {result.request_id})") + print("") + + # 显示 Schema 信息 + print("┌" + "─" * 68 + "┐") + print(f"│ 📋 Schema 信息{' ' * 52}│") + print("├" + "─" * 68 + "┤") + schema_name = result.profile.schema_name or "(未设置)" + schema_desc = result.profile.schema_description or "(未设置)" + print(f"│ 名称: {schema_name:<60}│") + print(f"│ 描述: {schema_desc:<60}│") + print("└" + "─" * 68 + "┘") + print("") + + # 显示用户画像 + if result.profile.attributes: + print("┌" + "─" * 68 + "┐") + print(f"│ 👤 用户画像(共 {len(result.profile.attributes)} 个字段){' ' * (49 - len(str(len(result.profile.attributes))))}│") + print("├" + "─" * 68 + "┤") + + for idx, attr in enumerate(result.profile.attributes, start=1): + value_display = attr.value if attr.value else "(暂未提取)" + + # 字段名 + print(f"│ [{idx}] {attr.name:<61}│") + + # 字段值(可能很长,需要处理) + if len(str(value_display)) <= 60: + print(f"│ 值: {str(value_display):<58}│") + else: + # 长文本分行显示 + value_str = str(value_display) + print(f"│ 值: {value_str[:58]:<58}│") + for i in range(58, len(value_str), 62): + line = value_str[i:i+62] + print(f"│ {line:<62}│") + + # 字段ID + id_display = truncate(attr.id, 58) + print(f"│ ID: {id_display:<58}│") + + # 分隔线(最后一个除外) + if idx < len(result.profile.attributes): + print("│" + " " * 68 + "│") + + print("└" + "─" * 68 + "┘") + print("") + + # 如果有字段被填充,添加说明 + has_values = any(attr.value for attr in result.profile.attributes) + if has_values: + print_success("💡 提示:上述画像信息是记忆服务自动从对话中提取的!") + print("") + else: + print_info("💡 提示:画像字段暂未填充,随着更多对话的积累,会逐步完善。") + print("") + else: + print_info("(暂无画像字段)") + print("") + + +async def step_delete_memory(delete_memory: DeleteMemory, end_user_id: str, node_ids: List[str]) -> None: + """删除指定的记忆条目""" + print_info("删除刚才新增的记忆条目,演示数据清理功能。") + print("") + + if not node_ids: + print_warn("⚠ 没有可删除的条目,跳过该步骤。") + return + + # 展示示例参数 + print_info("请求参数:") + print_info(f" · 用户ID:{end_user_id}") + print_info(f" · 待删除条目数:{len(node_ids)}") + print("") + + print_info(f"🗑️ 正在删除 {len(node_ids)} 条记忆...") + print("") + + for idx, node_id in enumerate(node_ids, start=1): + result = await delete_memory.arun( + DeleteMemoryInput(user_id=end_user_id, memory_node_id=node_id) + ) + print_success(f" ✓ [{idx}/{len(node_ids)}] 已删除:{truncate(node_id, 50)}") + print_info(f" 请求ID:{result.request_id}") + + print("") + print_success(f"✓ 全部删除完成,共删除 {len(node_ids)} 条记忆") + + + + +async def main() -> None: + # Required envs + require_env("DASHSCOPE_API_KEY") + end_user_id = get_env("END_USER_ID", "demo_user_test_001") + + # Initialize components + add_memory = AddMemory() + search_memory = SearchMemory() + list_memory = ListMemory() + delete_memory = DeleteMemory() + create_profile_schema = CreateProfileSchema() + get_user_profile = GetUserProfile() + llm = BaseLLM() + + try: + print_section("Demo 0: Create Profile Schema") + try: + schema_id = await step_create_profile_schema(create_profile_schema) + except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError) as e: + print_error("❌ 创建用户画像 Schema 失败:") + print_error(f" {format_api_error(e)}") + print_error("\n💡 建议:请检查 API Key 是否正确,或查看 Request ID 联系技术支持") + return + + print_section("Demo 1: Add Memory") + try: + node_ids = await step_add_memory(add_memory, end_user_id, schema_id) + except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError) as e: + print_error("❌ 添加记忆失败:") + print_error(f" {format_api_error(e)}") + print_error("\n💡 建议:请检查参数是否正确,或查看 Request ID 联系技术支持") + return + + # Wait for consistency + print("") + print_info("⏳ 等待记忆生成(3秒)...") + await asyncio.sleep(3) + print("") + + # 2. List memory + print_section("Demo 2: List Memory") + try: + existing_ids = await step_list_memory(list_memory, end_user_id) + except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError) as e: + print_error("❌ 列出记忆失败:") + print_error(f" {format_api_error(e)}") + # 非关键步骤,可以继续 + existing_ids = [] + + print_section("Demo 3: Search Memory + LLM Answer") + try: + _hits, _query = await step_search_memory_with_llm(search_memory, llm, end_user_id) + except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError) as e: + print_error("❌ 搜索记忆失败:") + print_error(f" {format_api_error(e)}") + # 非关键步骤,可以继续 + + # 等待用户画像提取完成 + print("") + print_info("⏳ 等待用户画像提取完成(2秒)...") + print_info(" 记忆服务正在从对话中提取用户信息(年龄、爱好等)...") + await asyncio.sleep(2) + print("") + + print_section("Demo 4: Get User Profile (展示自动提取的用户画像)") + try: + await step_get_user_profile(get_user_profile, schema_id, end_user_id) + except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError, MemoryNotFoundError) as e: + print_error("❌ 获取用户画像失败:") + print_error(f" {format_api_error(e)}") + # 非关键步骤,可以继续 + + print_section("Demo 5: Delete Memory") + try: + await step_delete_memory(delete_memory, end_user_id, node_ids) + except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError) as e: + print_error("❌ 删除记忆失败:") + print_error(f" {format_api_error(e)}") + # 非关键步骤,可以继续 + + # Wait for consistency + print("") + print_info("⏳ 等待删除生效(2秒)...") + await asyncio.sleep(2) + print("") + + print_section("Demo 6: List Memory Again (验证删除)") + try: + await step_list_memory(list_memory, end_user_id) + except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError) as e: + print_error("❌ 列出记忆失败:") + print_error(f" {format_api_error(e)}") + + print("") + print("=" * 70) + print_success("🎉 所有演示步骤已完成!") + print("=" * 70) + + finally: + # 清理资源:关闭所有 HTTP 连接 + print("") + print_info("🔄 正在清理资源...") + await add_memory.close() + await search_memory.close() + await list_memory.close() + await delete_memory.close() + await create_profile_schema.close() + await get_user_profile.close() + print_info("✓ 资源清理完成") + + +if __name__ == "__main__": + asyncio.run(main()) + + diff --git a/examples/memory/memory_demo.sh b/examples/memory/memory_demo.sh new file mode 100644 index 0000000..4414555 --- /dev/null +++ b/examples/memory/memory_demo.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 可选:首次运行时打开以下注释进行依赖安装 +# python3 -m venv .venv +# source .venv/bin/activate +# pip install -r requirements.txt + +# ===== 明文环境变量(示例值请替换) ===== +export LLM_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1" +export DASHSCOPE_API_KEY="YOUR_DASHSCOPE_API_KEY" +export MEMORY_SERVICE_ENDPOINT="https://dashscope.aliyuncs.com/api/v2/apps/memory" + +# ===== 日志配置 ===== +# 禁用详细日志,保持控制台输出清晰 +export LOG_LEVEL="${LOG_LEVEL:-WARNING}" +export PYTHONUNBUFFERED=1 # 确保 Python 输出实时显示(不缓冲) + +# ===== END_USER_ID 配置 ===== +# 方式1:直接在这里指定用户ID(如果需要固定ID,请取消注释并填写) +# END_USER_ID="your_custom_user_id" + +# 方式2:留空或不设置,自动生成格式:modelstudio_memory_user_MMDD_UUID(4位) +END_USER_ID="${END_USER_ID:-}" + +# 动态生成逻辑:如果 END_USER_ID 为空,则自动生成 +if [ -z "$END_USER_ID" ]; then + MMDD=$(date +%m%d) + # 生成4位随机UUID片段(使用uuidgen的前4位,或使用随机数) + if command -v uuidgen &> /dev/null; then + UUID4=$(uuidgen | tr '[:upper:]' '[:lower:]' | head -c 4) + else + # 如果没有uuidgen,使用随机数生成4位十六进制 + UUID4=$(printf '%04x' $((RANDOM % 65536))) + fi + export END_USER_ID="modelstudio_memory_user_${MMDD}_${UUID4}" + echo "[INFO] Generated END_USER_ID: $END_USER_ID" +else + echo "[INFO] Using existing END_USER_ID: $END_USER_ID" +fi + + +# memory_demo.sh 与 run_memory_demo.py 应位于同一目录 +WORK_DIR="${PWD}" +if [ ! -f "$WORK_DIR/memory_demo.py" ]; then + echo "[ERROR] memory_demo.py not found in $WORK_DIR" >&2 + echo "Please run this script in the same directory as memory_demo.py" >&2 + exit 1 +fi + +# 检查 API Key 是否设置 +if [ -z "$DASHSCOPE_API_KEY" ]; then + echo "[ERROR] DASHSCOPE_API_KEY is empty. Please set it before running." >&2 + exit 1 +fi + +# 为支持绝对包导入(agentscope_bricks.*),将仓库根目录加入 PYTHONPATH +REPO_ROOT="$(cd "$WORK_DIR/../.." && pwd)" +export PYTHONPATH="${REPO_ROOT}:${PYTHONPATH:-}" + +python "$WORK_DIR/memory_demo.py" | cat + + diff --git a/src/agentscope_bricks/components/memory/__init__.py b/src/agentscope_bricks/components/memory/__init__.py index e69de29..5cc97cb 100644 --- a/src/agentscope_bricks/components/memory/__init__.py +++ b/src/agentscope_bricks/components/memory/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" +Memory components for AgentScope Bricks. + +This package provides various memory implementations: +- modelstudio_memory: ModelStudio Memory service for cloud-based memory storage +- local_memory: Local in-memory storage +- redis_memory: Redis-based persistent memory storage + +Each memory implementation provides components for storing and retrieving +conversation history and user information. +""" + +__all__ = [] diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory.py b/src/agentscope_bricks/components/memory/modelstudio_memory.py deleted file mode 100644 index 272bfae..0000000 --- a/src/agentscope_bricks/components/memory/modelstudio_memory.py +++ /dev/null @@ -1,563 +0,0 @@ -# -*- coding: utf-8 -*- -import os -from typing import List, Dict, Any, Optional - -import aiohttp -from pydantic import BaseModel, Field, model_validator - -from agentscope_bricks.base.component import Component - -# ENV-PRE -MEMORY_SERVICE_ENDPOINT = os.getenv("MEMORY_SERVICE_ENDPOINT", "https://dashscope.aliyuncs.com/api/v2/apps/memory") -ADD_MEMORY_URL = f'{MEMORY_SERVICE_ENDPOINT}/add' -SEARCH_MEMORY_URL = f'{MEMORY_SERVICE_ENDPOINT}/memory_nodes/search' -LIST_MEMORY_URL = f'{MEMORY_SERVICE_ENDPOINT}/memory_nodes' -DELETE_MEMORY_URL_PATTERN = f'{MEMORY_SERVICE_ENDPOINT}/memory_nodes/{{memory_node_id}}' -CREATE_PROFILE_SCHEMA_URL = f"{MEMORY_SERVICE_ENDPOINT}/profile_schemas" -GET_USER_PROFILE_URL_PATTERN = f"{MEMORY_SERVICE_ENDPOINT}/profile_schemas/{{schema_id}}/user_profile" - - -class Message(BaseModel): - role: str - content: Any - - -class AddMemoryInput(BaseModel): - user_id: str = Field(..., description="end user id") - messages: List[Message] = Field(..., description="conversation messages") - timestamp: int = Field(..., description="timestamp of the memory") - meta_data: Optional[Dict[str, Any]] = Field( - None, - description="metadata including location and media description", - ) - - class Config: - extra = "allow" # Allow extra fields - - -class MemoryNode(BaseModel): - memory_node_id: Optional[str] = None - content: str - - -class AddMemoryOutput(BaseModel): - memory_nodes: List[MemoryNode] = Field( - ..., - description="generated memory nodes", - ) - - -class SearchFilters(BaseModel): - tags: Optional[List[str]] = Field(None, description="filter by tags") - - -class SearchMemoryInput(BaseModel): - user_id: str = Field(..., description="end user id") - messages: List[Message] = Field(..., description="conversation messages") - top_k: Optional[int] = Field( - 100, - description="number of results to return", - ) - min_score: Optional[float] = Field( - 0.0, - description="minimum score threshold", - ) - - class Config: - extra = "allow" # Allow extra fields - - -class SearchMemoryOutput(BaseModel): - memory_nodes: List[MemoryNode] = Field( - ..., - description="retrieved memory nodes", - ) - request_id: str = Field(..., description="request id") - - -class ListMemoryInput(BaseModel): - user_id: str = Field(..., description="end user id") - page_num: Optional[int] = Field(1, description="page number") - page_size: Optional[int] = Field( - 10, - description="number of items per page", - ) - - class Config: - extra = "allow" # Allow extra fields - - -class ListMemoryOutput(BaseModel): - memory_nodes: List[MemoryNode] = Field( - ..., - description="retrieved memory nodes", - ) - page_size: int = Field(..., description="number of items per page") - page_num: int = Field(..., description="current page number") - total: int = Field(..., description="total number of memory nodes") - request_id: str = Field(..., description="request id") - - -class DeleteMemoryInput(BaseModel): - user_id: str = Field(..., description="end user id") - memory_node_id: str = Field(..., description="memory node id to delete") - - class Config: - extra = "allow" # Allow extra fields - - -class DeleteMemoryOutput(BaseModel): - request_id: str = Field(..., description="request id") - - -class AddMemory(Component[AddMemoryInput, AddMemoryOutput]): - """ - Memory Component for storing conversation history as memory nodes. - """ - - name = "add_memory" - description = "Store conversation messages as memory nodes" - - def __init__(self) -> None: - super().__init__() - self.service_id = os.getenv("MODELSTUDIO_SERVICE_ID", "memory_service") - self.add_memory_url = ADD_MEMORY_URL - self.api_key = os.getenv("DASHSCOPE_API_KEY") - if not self.api_key: - raise ValueError( - "DASHSCOPE_API_KEY environment variable is required", - ) - - async def _arun( - self, - args: AddMemoryInput, - **kwargs: Any, - ) -> AddMemoryOutput: - """ - Add memory nodes - - Args: - args: AddMemoryInput - **kwargs: Additional parameters - - Returns: - AddMemoryOutput: Memory output - """ - try: - # Build request body - all fields including extra fields will be - # included - payload = args.model_dump(exclude_none=True) - - # Send request - async with aiohttp.ClientSession() as session: - async with session.post( - self.add_memory_url, - json=payload, - headers={ - "Content-Type": "application/json", - "User-Agent": "agentscope-bricks", - "Authorization": f"Bearer {self.api_key}", - }, - ) as response: - if response.status != 200: - error_text = await response.text() - raise Exception( - f"Add memory failed with status " - f"{response.status}: {error_text}", - ) - - result = await response.json() - return AddMemoryOutput( - memory_nodes=[ - MemoryNode(**node) - for node in result.get("memory_nodes", []) - ], - ) - - except Exception as e: - raise Exception(f"Error in AddMemory: {str(e)}") - - -class SearchMemory(Component[SearchMemoryInput, SearchMemoryOutput]): - """ - Memory Component for searching relevant memories based on conversation - context. - """ - - name = "search_memory" - description = "Search for relevant memories based on conversation context" - - def __init__(self) -> None: - super().__init__() - self.service_id = os.getenv("MODELSTUDIO_SERVICE_ID", "memory_service") - self.search_memory_url = SEARCH_MEMORY_URL - self.api_key = os.getenv("DASHSCOPE_API_KEY") - if not self.api_key: - raise ValueError( - "DASHSCOPE_API_KEY environment variable is required", - ) - - async def _arun( - self, - args: SearchMemoryInput, - **kwargs: Any, - ) -> SearchMemoryOutput: - """ - Search memory nodes - - Args: - args: SearchMemoryInput - **kwargs: Additional parameters - - Returns: - SearchMemoryOutput: Search output - """ - try: - # Build request body - all fields including extra fields will be - # included - payload = args.model_dump(exclude_none=True) - - # Send request - async with aiohttp.ClientSession() as session: - async with session.post( - self.search_memory_url, - json=payload, - headers={ - "Content-Type": "application/json", - "User-Agent": "agentscope-bricks", - "Authorization": f"Bearer {self.api_key}", - }, - ) as response: - if response.status != 200: - error_text = await response.text() - raise Exception( - f"Search memory failed with status " - f"{response.status}: {error_text}", - ) - - result = await response.json() - return SearchMemoryOutput( - memory_nodes=[ - MemoryNode(**node) - for node in result.get("memory_nodes", []) - ], - request_id=result.get("request_id", ""), - ) - - except Exception as e: - raise Exception(f"Error in SearchMemory: {str(e)}") - - -class ListMemory(Component[ListMemoryInput, ListMemoryOutput]): - """ - Memory Component for listing memory nodes for a user. - """ - - name = "list_memory" - description = "List memory nodes for a user" - - def __init__(self) -> None: - super().__init__() - self.service_id = os.getenv("MODELSTUDIO_SERVICE_ID", "memory_service") - self.list_memory_url = LIST_MEMORY_URL - self.api_key = os.getenv("DASHSCOPE_API_KEY") - if not self.api_key: - raise ValueError( - "DASHSCOPE_API_KEY environment variable is required", - ) - - async def _arun( - self, - args: ListMemoryInput, - **kwargs: Any, - ) -> ListMemoryOutput: - """ - List memory nodes for a user - - Args: - args: ListMemoryInput - **kwargs: Additional parameters - - Returns: - ListMemoryOutput: List memory output - """ - try: - # Build request body - all fields including extra fields will be - # included - payload = args.model_dump(exclude_none=True) - - async with aiohttp.ClientSession() as session: - async with session.get( - self.list_memory_url, - params=payload, - json={}, - headers={ - "Content-Type": "application/json", - "User-Agent": "agentscope-bricks", - "Authorization": f"Bearer {self.api_key}", - }, - ) as response: - if response.status != 200: - error_text = await response.text() - raise Exception( - f"List memory failed with status " - f"{response.status}: {error_text}", - ) - - result = await response.json() - return ListMemoryOutput( - memory_nodes=[ - MemoryNode(**node) - for node in result.get("memory_nodes", []) - ], - page_size=result.get("page_size", 10), - page_num=result.get("page_num", 1), - total=result.get("total", 0), - request_id=result.get("request_id", ""), - ) - - except Exception as e: - raise Exception(f"Error in ListMemory: {str(e)}") - - -class DeleteMemory(Component[DeleteMemoryInput, DeleteMemoryOutput]): - """ - Memory Component for deleting a specific memory node. - """ - - name = "delete_memory" - description = "Delete a specific memory node" - - def __init__(self) -> None: - super().__init__() - self.service_id = os.getenv("MODELSTUDIO_SERVICE_ID", "memory_service") - self.delete_memory_url_pattern = DELETE_MEMORY_URL_PATTERN - self.api_key = os.getenv("DASHSCOPE_API_KEY") - if not self.api_key: - raise ValueError( - "DASHSCOPE_API_KEY environment variable is required", - ) - - async def _arun( - self, - args: DeleteMemoryInput, - **kwargs: Any, - ) -> DeleteMemoryOutput: - """ - Delete a memory node - - Args: - args: DeleteMemoryInput - **kwargs: Additional parameters - - Returns: - DeleteMemoryOutput: Delete memory output - """ - try: - # Build URL with path param and include body as per example - url = self.delete_memory_url_pattern.format(memory_node_id=args.memory_node_id) - - async with aiohttp.ClientSession() as session: - async with session.delete( - url, - json={}, - headers={ - "Content-Type": "application/json", - "User-Agent": "agentscope-bricks", - "Authorization": f"Bearer {self.api_key}", - }, - ) as response: - if response.status != 200: - error_text = await response.text() - raise Exception( - f"Delete memory failed with status " - f"{response.status}: {error_text}", - ) - - result = await response.json() - return DeleteMemoryOutput( - request_id=result.get("request_id", ""), - ) - - except Exception as e: - raise Exception(f"Error in DeleteMemory: {str(e)}") - - -class ProfileAttribute(BaseModel): - name: str = Field(..., description='Attribute name') - description: Optional[str] = Field(None, description='Attribute description') - immutable: Optional[bool] = Field(False, description='Whether the attribute is immutable') - default_value: Optional[Any] = Field(None, description='Default value for the attribute') - - -class CreateProfileSchemaInput(BaseModel): - name: str = Field(..., description='Profile schema name') - description: Optional[str] = Field(None, description='Profile schema description') - attributes: List[ProfileAttribute] = Field(..., description='List of attribute definitions (>=1)') - - @model_validator(mode='after') - def validate_attributes(self) -> 'CreateProfileSchemaInput': - if not self.attributes or len(self.attributes) == 0: - raise ValueError('attributes must contain at least one item') - return self - - class Config: - extra = "allow" - - -class CreateProfileSchemaOutput(BaseModel): - profile_schema_id: str = Field(..., description='Created profile schema id') - request_id: str = Field(..., description='Request id') - - -class CreateProfileSchema(Component[CreateProfileSchemaInput, CreateProfileSchemaOutput]): - """ - Create a user profile schema with attribute definitions. - """ - - name = 'create_profile_schema' - description = 'Create a profile schema with attribute definitions' - - def __init__(self): - super().__init__() - self.service_id = os.getenv("MODELSTUDIO_SERVICE_ID", "profile_service") - self.create_profile_schema_url = CREATE_PROFILE_SCHEMA_URL - self.api_key = os.getenv("DASHSCOPE_API_KEY") - if not self.api_key: - raise ValueError("DASHSCOPE_API_KEY environment variable is required") - - async def _arun( - self, - args: CreateProfileSchemaInput, - **kwargs: Any - ) -> CreateProfileSchemaOutput: - """ - Create a profile schema - - Args: - args: CreateProfileSchemaInput - **kwargs: Additional parameters - - Returns: - CreateProfileSchemaOutput: Result including profile_schema_id and request_id - """ - try: - payload = args.model_dump(exclude_none=True) - - async with aiohttp.ClientSession() as session: - async with session.post( - self.create_profile_schema_url, - json=payload, - headers={ - "Content-Type": "application/json", - "User-Agent": "agentscope-bricks", - "Authorization": f"Bearer {self.api_key}" - } - ) as response: - if response.status != 200: - error_text = await response.text() - raise Exception(f"Create profile schema failed with status {response.status}: {error_text}") - - result = await response.json() - return CreateProfileSchemaOutput( - profile_schema_id=result.get('profile_schema_id', ''), - request_id=result.get('request_id', '') - ) - except Exception as e: - raise Exception(f"Error in CreateProfileSchema: {str(e)}") - - -class UserProfileAttribute(BaseModel): - name: str = Field(..., description='Attribute name') - id: str = Field(..., description='Attribute id') - value: Optional[Any] = Field(None, description='Attribute value') - - -class UserProfile(BaseModel): - schema_description: Optional[str] = Field(None, description='Schema description') - schema_name: Optional[str] = Field(None, description='Schema name') - attributes: List[UserProfileAttribute] = Field(default_factory=list, description='User attributes') - - -class GetUserProfileInput(BaseModel): - schema_id: str = Field(..., description='Profile schema id') - user_id: str = Field(..., description='End user id') - - -class GetUserProfileOutput(BaseModel): - request_id: str = Field(..., description='Request id') - profile: UserProfile = Field(..., description='User profile') - - -class GetUserProfile(Component[GetUserProfileInput, GetUserProfileOutput]): - """ - Get user profile by schema id and user id. - """ - - name = 'get_user_profile' - description = 'Get user profile by schema id and user id' - - def __init__(self): - super().__init__() - self.service_id = os.getenv("MODELSTUDIO_SERVICE_ID", "profile_service") - self.get_user_profile_url_pattern = GET_USER_PROFILE_URL_PATTERN - self.api_key = os.getenv("DASHSCOPE_API_KEY") - if not self.api_key: - raise ValueError("DASHSCOPE_API_KEY environment variable is required") - - async def _arun( - self, - args: GetUserProfileInput, - **kwargs: Any - ) -> GetUserProfileOutput: - """ - Get a user profile - - Args: - args: GetUserProfileInput - **kwargs: Additional parameters - - Returns: - GetUserProfileOutput: Profile and request id - """ - try: - # Build URL without user_id in path; user_id will be a query parameter - url = self.get_user_profile_url_pattern.format(schema_id=args.schema_id) - - async with aiohttp.ClientSession() as session: - async with session.get( - url, - params={"user_id": args.user_id}, - json={}, - headers={ - "Content-Type": "application/json", - "User-Agent": "agentscope-bricks", - "Authorization": f"Bearer {self.api_key}" - } - ) as response: - if response.status != 200: - error_text = await response.text() - raise Exception(f"Get user profile failed with status {response.status}: {error_text}") - - result = await response.json() - profile_raw = result.get('profile', {}) - attributes = [ - UserProfileAttribute( - name=item.get('name', ''), - id=item.get('id', ''), - value=item.get('value') - ) for item in profile_raw.get('attributes', []) - ] - - profile = UserProfile( - schema_description=profile_raw.get('schemaDescription'), - schema_name=profile_raw.get('schemaName'), - attributes=attributes - ) - - return GetUserProfileOutput( - request_id=result.get('requestId', ''), - profile=profile - ) - except Exception as e: - raise Exception(f"Error in GetUserProfile: {str(e)}") diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory/__init__.py b/src/agentscope_bricks/components/memory/modelstudio_memory/__init__.py new file mode 100644 index 0000000..a46d30b --- /dev/null +++ b/src/agentscope_bricks/components/memory/modelstudio_memory/__init__.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +""" +ModelStudio Memory Components. + +This package provides components for interacting with the ModelStudio Memory +service. + +Components: + - AddMemory: Store conversation messages as memory nodes + - SearchMemory: Search for relevant memories + - ListMemory: List memory nodes with pagination + - DeleteMemory: Delete a specific memory node + - CreateProfileSchema: Create a user profile schema + - GetUserProfile: Retrieve a user profile + +Models: + All Pydantic models for input/output are available in the models submodule. + +Exceptions: + Custom exceptions for better error handling are available in the + exceptions submodule. + +Configuration: + Configuration can be managed through environment variables or by + providing a MemoryServiceConfig instance. +""" + +# Configuration +from .config import MemoryServiceConfig + +# Exceptions +from .exceptions import ( + MemoryAPIError, + MemoryAuthenticationError, + MemoryNetworkError, + MemoryNotFoundError, + MemoryValidationError, +) + +# Components +from .core import ( + AddMemory, + SearchMemory, + ListMemory, + DeleteMemory, + CreateProfileSchema, + GetUserProfile, +) + +# Models - Import commonly used models for convenience +from .models import ( + AddMemoryInput, + AddMemoryOutput, + CreateProfileSchemaInput, + CreateProfileSchemaOutput, + DeleteMemoryInput, + DeleteMemoryOutput, + GetUserProfileInput, + GetUserProfileOutput, + ListMemoryInput, + ListMemoryOutput, + MemoryNode, + Message, + ProfileAttribute, + SearchMemoryInput, + SearchMemoryOutput, + UserProfile, + UserProfileAttribute, +) + +__all__ = [ + # Core Components + "AddMemory", + "SearchMemory", + "ListMemory", + "DeleteMemory", + "CreateProfileSchema", + "GetUserProfile", + # Configuration + "MemoryServiceConfig", + # Exceptions + "MemoryAPIError", + "MemoryAuthenticationError", + "MemoryNetworkError", + "MemoryNotFoundError", + "MemoryValidationError", + # Models + "Message", + "MemoryNode", + "AddMemoryInput", + "AddMemoryOutput", + "SearchMemoryInput", + "SearchMemoryOutput", + "ListMemoryInput", + "ListMemoryOutput", + "DeleteMemoryInput", + "DeleteMemoryOutput", + "ProfileAttribute", + "CreateProfileSchemaInput", + "CreateProfileSchemaOutput", + "UserProfileAttribute", + "UserProfile", + "GetUserProfileInput", + "GetUserProfileOutput", +] + diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory/base.py b/src/agentscope_bricks/components/memory/modelstudio_memory/base.py new file mode 100644 index 0000000..c4fed84 --- /dev/null +++ b/src/agentscope_bricks/components/memory/modelstudio_memory/base.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +""" +Base class for ModelStudio Memory components. +""" +import logging +from typing import Any, Dict, Optional + +import aiohttp + +from agentscope_bricks.components.memory.modelstudio_memory.config import MemoryServiceConfig +from agentscope_bricks.components.memory.modelstudio_memory.exceptions import ( + MemoryAPIError, + MemoryAuthenticationError, + MemoryNetworkError, + MemoryNotFoundError, + MemoryValidationError, +) + +logger = logging.getLogger(__name__) + + +class ModelStudioMemoryBase: + """ + Base class for ModelStudio Memory API components. + + This class provides common functionality for all memory components, + including: + - Configuration management + - HTTP request handling with error handling + - Common headers generation + - Session management + + Attributes: + config: Configuration for the memory service + """ + + def __init__(self, config: Optional[MemoryServiceConfig] = None): + """ + Initialize the base memory component. + + Args: + config: Optional configuration. If not provided, will be loaded + from environment variables. + + Raises: + ValueError: If required configuration is missing + """ + self.config = config or MemoryServiceConfig.from_env() + self._session: Optional[aiohttp.ClientSession] = None + + def _get_headers(self) -> Dict[str, str]: + """ + Get common HTTP headers for API requests. + + Returns: + Dictionary of HTTP headers + """ + return { + "Content-Type": "application/json", + "User-Agent": "agentscope-bricks", + "Authorization": f"Bearer {self.config.api_key}", + } + + async def _get_session(self) -> aiohttp.ClientSession: + """ + Get or create an aiohttp session. + + Returns: + An aiohttp ClientSession + """ + if self._session is None or self._session.closed: + self._session = aiohttp.ClientSession() + return self._session + + async def _request( + self, + method: str, + url: str, + **kwargs: Any, + ) -> Dict[str, Any]: + """ + Common HTTP request handler with comprehensive error handling. + + Args: + method: HTTP method (GET, POST, DELETE, etc.) + url: Request URL + **kwargs: Additional arguments for the request + + Returns: + Response JSON as dictionary + + Raises: + MemoryAuthenticationError: If authentication fails (401) + MemoryNotFoundError: If resource not found (404) + MemoryAPIError: For other API errors + MemoryNetworkError: For network-related errors + """ + try: + session = await self._get_session() + logger.debug(f"Making {method} request to {url}") + + async with session.request( + method, + url, + headers=self._get_headers(), + **kwargs, + ) as response: + # Handle successful response + if response.status == 200: + result = await response.json() + logger.debug( + f"Request successful: {method} {url}", + ) + return result + + # Handle error responses (4XX, 5XX) + # Try to parse JSON error response first + error_data = None + try: + error_data = await response.json() + except Exception: + # If JSON parsing fails, fall back to text + error_text = await response.text() + error_data = {"message": error_text} + + # Extract error information + error_code = error_data.get("code", "Unknown") + error_message = error_data.get("message", "Unknown error") + request_id = error_data.get("request_id", "") + + # Format error log + error_log = ( + f"API Error - Status: {response.status}, " + f"Code: {error_code}, Message: {error_message}, " + f"Request ID: {request_id}" + ) + logger.error(error_log) + + # Raise appropriate exception based on status code + if response.status == 401 or response.status == 403: + raise MemoryAuthenticationError( + error_message, + status_code=response.status, + error_code=error_code, + request_id=request_id, + ) + elif response.status == 404: + raise MemoryNotFoundError( + error_message, + status_code=response.status, + error_code=error_code, + request_id=request_id, + ) + elif response.status == 400: + raise MemoryValidationError( + error_message, + status_code=response.status, + error_code=error_code, + request_id=request_id, + ) + elif 400 <= response.status < 500: + # Other 4XX errors + raise MemoryValidationError( + error_message, + status_code=response.status, + error_code=error_code, + request_id=request_id, + ) + else: + # 5XX server errors + raise MemoryAPIError( + error_message, + status_code=response.status, + error_code=error_code, + request_id=request_id, + ) + + except aiohttp.ClientError as e: + logger.exception(f"Network error: {str(e)}") + raise MemoryNetworkError( + f"Network error during {method} request to {url}: {str(e)}", + ) from e + except ( + MemoryAuthenticationError, + MemoryNotFoundError, + MemoryValidationError, + MemoryAPIError, + ): + # Re-raise our custom exceptions (already have proper error info) + raise + except Exception as e: + logger.exception(f"Unexpected error: {str(e)}") + raise MemoryAPIError( + f"Unexpected error during {method} request to {url}: {str(e)}", + ) from e + + async def close(self) -> None: + """ + Close the HTTP session. + + Should be called when the component is no longer needed to clean up + resources. + """ + if self._session and not self._session.closed: + await self._session.close() + logger.debug("Session closed") + + async def __aenter__(self): + """Support async context manager.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Support async context manager.""" + await self.close() + diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory/config.py b/src/agentscope_bricks/components/memory/modelstudio_memory/config.py new file mode 100644 index 0000000..c7321de --- /dev/null +++ b/src/agentscope_bricks/components/memory/modelstudio_memory/config.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +""" +Configuration management for ModelStudio Memory service. +""" +import os +from dataclasses import dataclass + + +# Default endpoint +DEFAULT_MEMORY_SERVICE_ENDPOINT = ( + "https://dashscope.aliyuncs.com/api/v2/apps/memory" +) + + +@dataclass +class MemoryServiceConfig: + """ + Configuration for ModelStudio Memory Service. + + Attributes: + api_key: DashScope API key for authentication + service_endpoint: Base URL for the memory service API + service_id: Service identifier + """ + + api_key: str + service_endpoint: str = DEFAULT_MEMORY_SERVICE_ENDPOINT + service_id: str = "memory_service" + + @classmethod + def from_env(cls) -> "MemoryServiceConfig": + """ + Create configuration from environment variables. + + Environment Variables: + DASHSCOPE_API_KEY: Required. API key for authentication + MEMORY_SERVICE_ENDPOINT: Optional. API endpoint URL + MODELSTUDIO_SERVICE_ID: Optional. Service identifier + + Returns: + MemoryServiceConfig: Configuration instance + + Raises: + ValueError: If DASHSCOPE_API_KEY is not set + """ + api_key = os.getenv("DASHSCOPE_API_KEY") + if not api_key: + raise ValueError( + "DASHSCOPE_API_KEY environment variable is required. " + "Please set it before using ModelStudio Memory components." + ) + + return cls( + api_key=api_key, + service_endpoint=os.getenv( + "MEMORY_SERVICE_ENDPOINT", + DEFAULT_MEMORY_SERVICE_ENDPOINT, + ), + service_id=os.getenv("MODELSTUDIO_SERVICE_ID", "memory_service"), + ) + + def get_add_memory_url(self) -> str: + """Get URL for adding memory.""" + return f"{self.service_endpoint}/add" + + def get_search_memory_url(self) -> str: + """Get URL for searching memory.""" + return f"{self.service_endpoint}/memory_nodes/search" + + def get_list_memory_url(self) -> str: + """Get URL for listing memory.""" + return f"{self.service_endpoint}/memory_nodes" + + def get_delete_memory_url(self, memory_node_id: str) -> str: + """Get URL for deleting a specific memory node.""" + return f"{self.service_endpoint}/memory_nodes/{memory_node_id}" + + def get_create_profile_schema_url(self) -> str: + """Get URL for creating profile schema.""" + return f"{self.service_endpoint}/profile_schemas" + + def get_user_profile_url(self, schema_id: str) -> str: + """Get URL for getting user profile.""" + return f"{self.service_endpoint}/profile_schemas/{schema_id}/user_profile" + diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory/core.py b/src/agentscope_bricks/components/memory/modelstudio_memory/core.py new file mode 100644 index 0000000..ab05771 --- /dev/null +++ b/src/agentscope_bricks/components/memory/modelstudio_memory/core.py @@ -0,0 +1,594 @@ +# -*- coding: utf-8 -*- +""" +ModelStudio Memory Components. + +This module provides components for interacting with the ModelStudio Memory +service, enabling: +- Adding conversation memories +- Searching for relevant memories +- Listing and managing memory nodes +- Creating and retrieving user profiles + +All components support async operations and follow the Component pattern. +""" +import logging +from typing import Any, Optional + +from agentscope_bricks.base.component import Component + +from .base import ModelStudioMemoryBase +from .config import MemoryServiceConfig +from .models import ( + AddMemoryInput, + AddMemoryOutput, + CreateProfileSchemaInput, + CreateProfileSchemaOutput, + DeleteMemoryInput, + DeleteMemoryOutput, + GetUserProfileInput, + GetUserProfileOutput, + ListMemoryInput, + ListMemoryOutput, + MemoryNode, + SearchMemoryInput, + SearchMemoryOutput, + UserProfile, + UserProfileAttribute, +) + +logger = logging.getLogger(__name__) + + +class AddMemory( + Component[AddMemoryInput, AddMemoryOutput], + ModelStudioMemoryBase, +): + """ + Component for storing conversation history as memory nodes. + + This component sends conversation messages to the ModelStudio Memory service + to be processed and stored as searchable memory nodes. The service + automatically extracts and structures relevant information. + + Environment Variables: + DASHSCOPE_API_KEY: Required. API key for authentication + MODELSTUDIO_SERVICE_ID: Optional. Service identifier + (default: "memory_service") + MEMORY_SERVICE_ENDPOINT: Optional. API endpoint URL + (default: https://dashscope.aliyuncs.com/api/v2/apps/memory) + + Raises: + ValueError: If DASHSCOPE_API_KEY is not set + MemoryAPIError: If the API request fails + MemoryAuthenticationError: If authentication fails + MemoryNetworkError: If network communication fails + """ + + name = "add_memory" + description = "Store conversation messages as memory nodes" + + def __init__(self, config: Optional[MemoryServiceConfig] = None) -> None: + """ + Initialize the AddMemory component. + + Args: + config: Optional configuration. If not provided, will be loaded + from environment variables. + """ + Component.__init__(self) + ModelStudioMemoryBase.__init__(self, config) + + async def _arun( + self, + args: AddMemoryInput, + **kwargs: Any, + ) -> AddMemoryOutput: + """ + Add memory nodes for the given conversation. + + Args: + args: Input containing user_id, messages, timestamp, and optional + metadata + **kwargs: Additional parameters (currently unused) + + Returns: + AddMemoryOutput containing the created memory nodes and request_id + + Raises: + MemoryAPIError: If the API request fails + """ + logger.info(f"Adding memory for user {args.user_id}") + + try: + # Build request payload + payload = args.model_dump(exclude_none=True) + + # Send request + result = await self._request( + "POST", + self.config.get_add_memory_url(), + json=payload, + ) + + # Debug: print API response structure + logger.debug(f"API Response: {result}") + logger.debug(f"memory_nodes type: {type(result.get('memory_nodes'))}") + logger.debug(f"memory_nodes value: {result.get('memory_nodes')}") + + # Parse response - handle both list and dict formats + memory_nodes_raw = result.get("memory_nodes", []) + if isinstance(memory_nodes_raw, dict): + # If it's a dict (single node), wrap it in a list + memory_nodes_list = [memory_nodes_raw] + elif isinstance(memory_nodes_raw, list): + memory_nodes_list = memory_nodes_raw + else: + memory_nodes_list = [] + + output = AddMemoryOutput( + memory_nodes=[ + MemoryNode(**node) + for node in memory_nodes_list + ], + request_id=result.get("request_id", ""), + ) + + logger.info( + f"Successfully added {len(output.memory_nodes)} memory nodes", + ) + return output + + except Exception as e: + logger.exception(f"Failed to add memory for user {args.user_id}") + raise + + +class SearchMemory( + Component[SearchMemoryInput, SearchMemoryOutput], + ModelStudioMemoryBase, +): + """ + Component for searching relevant memories based on conversation context. + + This component searches the memory database for relevant past conversations + and information based on the current conversation context. + + Environment Variables: + DASHSCOPE_API_KEY: Required. API key for authentication + MODELSTUDIO_SERVICE_ID: Optional. Service identifier + MEMORY_SERVICE_ENDPOINT: Optional. API endpoint URL + + Raises: + ValueError: If DASHSCOPE_API_KEY is not set + MemoryAPIError: If the API request fails + """ + + name = "search_memory" + description = "Search for relevant memories based on conversation context" + + def __init__(self, config: Optional[MemoryServiceConfig] = None) -> None: + """ + Initialize the SearchMemory component. + + Args: + config: Optional configuration. If not provided, will be loaded + from environment variables. + """ + Component.__init__(self) + ModelStudioMemoryBase.__init__(self, config) + + async def _arun( + self, + args: SearchMemoryInput, + **kwargs: Any, + ) -> SearchMemoryOutput: + """ + Search for relevant memory nodes. + + Args: + args: Input containing user_id, messages, top_k, and min_score + **kwargs: Additional parameters (currently unused) + + Returns: + SearchMemoryOutput containing retrieved memory nodes and request_id + + Raises: + MemoryAPIError: If the API request fails + """ + logger.info( + f"Searching memory for user {args.user_id} " + f"(top_k={args.top_k}, min_score={args.min_score})", + ) + + try: + # Build request payload + payload = args.model_dump(exclude_none=True) + + # Send request + result = await self._request( + "POST", + self.config.get_search_memory_url(), + json=payload, + ) + + # Parse response + output = SearchMemoryOutput( + memory_nodes=[ + MemoryNode(**node) + for node in result.get("memory_nodes", []) + ], + request_id=result.get("request_id", ""), + ) + + logger.info( + f"Found {len(output.memory_nodes)} memory nodes for " + f"user {args.user_id}", + ) + return output + + except Exception as e: + logger.exception( + f"Failed to search memory for user {args.user_id}", + ) + raise + + +class ListMemory( + Component[ListMemoryInput, ListMemoryOutput], + ModelStudioMemoryBase, +): + """ + Component for listing memory nodes with pagination. + + This component retrieves a paginated list of all memory nodes for a + specific user. + + Environment Variables: + DASHSCOPE_API_KEY: Required. API key for authentication + MODELSTUDIO_SERVICE_ID: Optional. Service identifier + MEMORY_SERVICE_ENDPOINT: Optional. API endpoint URL + + Raises: + ValueError: If DASHSCOPE_API_KEY is not set + MemoryAPIError: If the API request fails + """ + + name = "list_memory" + description = "List memory nodes for a user with pagination" + + def __init__(self, config: Optional[MemoryServiceConfig] = None) -> None: + """ + Initialize the ListMemory component. + + Args: + config: Optional configuration. If not provided, will be loaded + from environment variables. + """ + Component.__init__(self) + ModelStudioMemoryBase.__init__(self, config) + + async def _arun( + self, + args: ListMemoryInput, + **kwargs: Any, + ) -> ListMemoryOutput: + """ + List memory nodes for a user with pagination. + + Args: + args: Input containing user_id, page_num, and page_size + **kwargs: Additional parameters (currently unused) + + Returns: + ListMemoryOutput containing memory nodes, pagination info, + and request_id + + Raises: + MemoryAPIError: If the API request fails + """ + logger.info( + f"Listing memory for user {args.user_id} " + f"(page {args.page_num}, size {args.page_size})", + ) + + try: + # Build request params + params = args.model_dump(exclude_none=True) + + # Send request (GET with query parameters) + result = await self._request( + "GET", + self.config.get_list_memory_url(), + params=params, + ) + + # Parse response + output = ListMemoryOutput( + memory_nodes=[ + MemoryNode(**node) + for node in result.get("memory_nodes", []) + ], + page_size=result.get("page_size", args.page_size or 10), + page_num=result.get("page_num", args.page_num or 1), + total=result.get("total", 0), + request_id=result.get("request_id", ""), + ) + + logger.info( + f"Retrieved {len(output.memory_nodes)} memory nodes " + f"(total: {output.total})", + ) + return output + + except Exception as e: + logger.exception(f"Failed to list memory for user {args.user_id}") + raise + + +class DeleteMemory( + Component[DeleteMemoryInput, DeleteMemoryOutput], + ModelStudioMemoryBase, +): + """ + Component for deleting a specific memory node. + + This component deletes a memory node by its ID. + + Environment Variables: + DASHSCOPE_API_KEY: Required. API key for authentication + MODELSTUDIO_SERVICE_ID: Optional. Service identifier + MEMORY_SERVICE_ENDPOINT: Optional. API endpoint URL + + Raises: + ValueError: If DASHSCOPE_API_KEY is not set + MemoryAPIError: If the API request fails + MemoryNotFoundError: If the memory node is not found + """ + + name = "delete_memory" + description = "Delete a specific memory node" + + def __init__(self, config: Optional[MemoryServiceConfig] = None) -> None: + """ + Initialize the DeleteMemory component. + + Args: + config: Optional configuration. If not provided, will be loaded + from environment variables. + """ + Component.__init__(self) + ModelStudioMemoryBase.__init__(self, config) + + async def _arun( + self, + args: DeleteMemoryInput, + **kwargs: Any, + ) -> DeleteMemoryOutput: + """ + Delete a memory node. + + Args: + args: Input containing user_id and memory_node_id + **kwargs: Additional parameters (currently unused) + + Returns: + DeleteMemoryOutput containing the request_id + + Raises: + MemoryAPIError: If the API request fails + MemoryNotFoundError: If the memory node is not found + """ + logger.info( + f"Deleting memory node {args.memory_node_id} " + f"for user {args.user_id}", + ) + + try: + # Build URL with path parameter + url = self.config.get_delete_memory_url(args.memory_node_id) + + # Send request + result = await self._request("DELETE", url) + + # Parse response + output = DeleteMemoryOutput( + request_id=result.get("request_id", ""), + ) + + logger.info( + f"Successfully deleted memory node {args.memory_node_id}", + ) + return output + + except Exception as e: + logger.exception( + f"Failed to delete memory node {args.memory_node_id}", + ) + raise + + +class CreateProfileSchema( + Component[CreateProfileSchemaInput, CreateProfileSchemaOutput], + ModelStudioMemoryBase, +): + """ + Component for creating a user profile schema. + + This component creates a schema that defines the structure of user profiles, + including attribute definitions. + + Environment Variables: + DASHSCOPE_API_KEY: Required. API key for authentication + MODELSTUDIO_SERVICE_ID: Optional. Service identifier + MEMORY_SERVICE_ENDPOINT: Optional. API endpoint URL + + Raises: + ValueError: If DASHSCOPE_API_KEY is not set or if attributes list + is empty + MemoryAPIError: If the API request fails + """ + + name = "create_profile_schema" + description = "Create a profile schema with attribute definitions" + + def __init__(self, config: Optional[MemoryServiceConfig] = None) -> None: + """ + Initialize the CreateProfileSchema component. + + Args: + config: Optional configuration. If not provided, will be loaded + from environment variables. + """ + Component.__init__(self) + ModelStudioMemoryBase.__init__(self, config) + + async def _arun( + self, + args: CreateProfileSchemaInput, + **kwargs: Any, + ) -> CreateProfileSchemaOutput: + """ + Create a profile schema. + + Args: + args: Input containing name, description, and attributes + **kwargs: Additional parameters (currently unused) + + Returns: + CreateProfileSchemaOutput containing profile_schema_id and + request_id + + Raises: + MemoryAPIError: If the API request fails + """ + logger.info(f"Creating profile schema: {args.name}") + + try: + # Build request payload + payload = args.model_dump(exclude_none=True) + + # Send request + result = await self._request( + "POST", + self.config.get_create_profile_schema_url(), + json=payload, + ) + + # Parse response + output = CreateProfileSchemaOutput( + profile_schema_id=result.get("profile_schema_id", ""), + request_id=result.get("request_id", ""), + ) + + logger.info( + f"Successfully created profile schema: " + f"{output.profile_schema_id}", + ) + return output + + except Exception as e: + logger.exception(f"Failed to create profile schema: {args.name}") + raise + + +class GetUserProfile( + Component[GetUserProfileInput, GetUserProfileOutput], + ModelStudioMemoryBase, +): + """ + Component for retrieving a user profile. + + This component retrieves a user's profile based on a schema ID and user ID. + + Environment Variables: + DASHSCOPE_API_KEY: Required. API key for authentication + MODELSTUDIO_SERVICE_ID: Optional. Service identifier + MEMORY_SERVICE_ENDPOINT: Optional. API endpoint URL + + Raises: + ValueError: If DASHSCOPE_API_KEY is not set + MemoryAPIError: If the API request fails + MemoryNotFoundError: If the profile is not found + """ + + name = "get_user_profile" + description = "Get user profile by schema id and user id" + + def __init__(self, config: Optional[MemoryServiceConfig] = None) -> None: + """ + Initialize the GetUserProfile component. + + Args: + config: Optional configuration. If not provided, will be loaded + from environment variables. + """ + Component.__init__(self) + ModelStudioMemoryBase.__init__(self, config) + + async def _arun( + self, + args: GetUserProfileInput, + **kwargs: Any, + ) -> GetUserProfileOutput: + """ + Get a user profile. + + Args: + args: Input containing schema_id and user_id + **kwargs: Additional parameters (currently unused) + + Returns: + GetUserProfileOutput containing the profile and request_id + + Raises: + MemoryAPIError: If the API request fails + MemoryNotFoundError: If the profile is not found + """ + logger.info( + f"Getting user profile for user {args.user_id} " + f"with schema {args.schema_id}", + ) + + try: + # Build URL with path parameter + url = self.config.get_user_profile_url(args.schema_id) + + # Send request with user_id as query parameter + result = await self._request( + "GET", + url, + params={"user_id": args.user_id}, + ) + + # Parse response - handle API's camelCase field names + profile_raw = result.get("profile", {}) + attributes = [ + UserProfileAttribute( + name=item.get("name", ""), + id=item.get("id", ""), + value=item.get("value"), + ) + for item in profile_raw.get("attributes", []) + ] + + profile = UserProfile( + schema_description=profile_raw.get("schemaDescription"), + schema_name=profile_raw.get("schemaName"), + attributes=attributes, + ) + + output = GetUserProfileOutput( + profile=profile, + request_id=result.get("requestId", ""), + ) + + logger.info( + f"Successfully retrieved profile for user {args.user_id}", + ) + return output + + except Exception as e: + logger.exception( + f"Failed to get profile for user {args.user_id}", + ) + raise diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory/exceptions.py b/src/agentscope_bricks/components/memory/modelstudio_memory/exceptions.py new file mode 100644 index 0000000..1a124c6 --- /dev/null +++ b/src/agentscope_bricks/components/memory/modelstudio_memory/exceptions.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +""" +Custom exceptions for ModelStudio Memory components. +""" +from typing import Optional + + +class MemoryAPIError(Exception): + """ + Base exception for Memory API errors. + + Attributes: + message: Error message + status_code: HTTP status code + error_code: API error code (e.g., 'InvalidApiKey', 'InvalidParameter') + request_id: Request ID for tracking + """ + + def __init__( + self, + message: str, + status_code: Optional[int] = None, + error_code: Optional[str] = None, + request_id: Optional[str] = None, + ): + self.status_code = status_code + self.error_code = error_code + self.request_id = request_id + super().__init__(message) + + def __str__(self) -> str: + """Format error message with all available information.""" + parts = [super().__str__()] + + if self.error_code: + parts.append(f"Error Code: {self.error_code}") + + if self.status_code: + parts.append(f"Status Code: {self.status_code}") + + if self.request_id: + parts.append(f"Request ID: {self.request_id}") + + return " | ".join(parts) + + +class MemoryAuthenticationError(MemoryAPIError): + """Raised when authentication fails (401, 403).""" + + pass + + +class MemoryNotFoundError(MemoryAPIError): + """Raised when a memory node is not found (404).""" + + pass + + +class MemoryValidationError(MemoryAPIError): + """Raised when input validation fails (400).""" + + pass + + +class MemoryNetworkError(MemoryAPIError): + """Raised when network communication fails.""" + + pass + diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory/models.py b/src/agentscope_bricks/components/memory/modelstudio_memory/models.py new file mode 100644 index 0000000..a642d4b --- /dev/null +++ b/src/agentscope_bricks/components/memory/modelstudio_memory/models.py @@ -0,0 +1,246 @@ +# -*- coding: utf-8 -*- +""" +Pydantic models for ModelStudio Memory API. +""" +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field, model_validator + + +# ==================== Message ==================== +class Message(BaseModel): + """Message in a conversation.""" + + role: str = Field(..., description="Role of the message sender") + content: Any = Field(..., description="Content of the message") + + +# ==================== Memory Node ==================== +class MemoryNode(BaseModel): + """A memory node stored in the system.""" + + memory_node_id: Optional[str] = Field( + None, + description="Unique identifier for the memory node", + ) + content: str = Field(..., description="Content of the memory node") + + +# ==================== Add Memory ==================== +class AddMemoryInput(BaseModel): + """Input for adding memory.""" + + user_id: str = Field(..., description="End user id") + messages: List[Message] = Field( + ..., + description="Conversation messages to be stored as memory", + ) + timestamp: int = Field(..., description="Timestamp of the memory") + meta_data: Optional[Dict[str, Any]] = Field( + None, + description="Optional metadata including location and media description", + ) + + class Config: + extra = "allow" # Allow extra fields + + +class AddMemoryOutput(BaseModel): + """Output from adding memory.""" + + memory_nodes: List[MemoryNode] = Field( + ..., + description="Generated memory nodes", + ) + request_id: str = Field(..., description="Request id") + + +# ==================== Search Memory ==================== +class SearchFilters(BaseModel): + """Filters for memory search.""" + + tags: Optional[List[str]] = Field( + None, + description="Filter results by tags", + ) + + +class SearchMemoryInput(BaseModel): + """Input for searching memory.""" + + user_id: str = Field(..., description="End user id") + messages: List[Message] = Field( + ..., + description="Conversation messages for context", + ) + top_k: Optional[int] = Field( + 100, + description="Maximum number of results to return", + ) + min_score: Optional[float] = Field( + 0.0, + description="Minimum similarity score threshold", + ) + + class Config: + extra = "allow" # Allow extra fields + + +class SearchMemoryOutput(BaseModel): + """Output from searching memory.""" + + memory_nodes: List[MemoryNode] = Field( + ..., + description="Retrieved memory nodes", + ) + request_id: str = Field(..., description="Request id") + + +# ==================== List Memory ==================== +class ListMemoryInput(BaseModel): + """Input for listing memory nodes.""" + + user_id: str = Field(..., description="End user id") + page_num: Optional[int] = Field(1, description="Page number (1-based)") + page_size: Optional[int] = Field( + 10, + description="Number of items per page", + ) + + class Config: + extra = "allow" # Allow extra fields + + +class ListMemoryOutput(BaseModel): + """Output from listing memory nodes.""" + + memory_nodes: List[MemoryNode] = Field( + ..., + description="Retrieved memory nodes", + ) + page_size: int = Field(..., description="Number of items per page") + page_num: int = Field(..., description="Current page number") + total: int = Field(..., description="Total number of memory nodes") + request_id: str = Field(..., description="Request id") + + +# ==================== Delete Memory ==================== +class DeleteMemoryInput(BaseModel): + """Input for deleting a memory node.""" + + user_id: str = Field(..., description="End user id") + memory_node_id: str = Field( + ..., + description="Memory node id to delete", + ) + + class Config: + extra = "allow" # Allow extra fields + + +class DeleteMemoryOutput(BaseModel): + """Output from deleting a memory node.""" + + request_id: str = Field(..., description="Request id") + + +# ==================== Profile Schema ==================== +class ProfileAttribute(BaseModel): + """Attribute definition in a profile schema.""" + + name: str = Field(..., description="Attribute name") + description: Optional[str] = Field( + None, + description="Attribute description", + ) + immutable: Optional[bool] = Field( + False, + description="Whether the attribute is immutable", + ) + default_value: Optional[Any] = Field( + None, + description="Default value for the attribute", + ) + + +class CreateProfileSchemaInput(BaseModel): + """Input for creating a profile schema.""" + + name: str = Field(..., description="Profile schema name") + description: Optional[str] = Field( + None, + description="Profile schema description", + ) + attributes: List[ProfileAttribute] = Field( + ..., + description="List of attribute definitions (must have at least 1)", + ) + + @model_validator(mode="after") + def validate_attributes(self) -> "CreateProfileSchemaInput": + """Validate that at least one attribute is provided.""" + if not self.attributes or len(self.attributes) == 0: + raise ValueError("attributes must contain at least one item") + return self + + class Config: + extra = "allow" + + +class CreateProfileSchemaOutput(BaseModel): + """Output from creating a profile schema.""" + + profile_schema_id: str = Field( + ..., + description="Created profile schema id", + ) + request_id: str = Field(..., description="Request id") + + +# ==================== User Profile ==================== +class UserProfileAttribute(BaseModel): + """Attribute in a user profile.""" + + name: str = Field(..., description="Attribute name") + id: str = Field(..., description="Attribute id") + value: Optional[Any] = Field(None, description="Attribute value") + + +class UserProfile(BaseModel): + """User profile with attributes.""" + + schema_description: Optional[str] = Field( + None, + alias="schemaDescription", + description="Schema description", + ) + schema_name: Optional[str] = Field( + None, + alias="schemaName", + description="Schema name", + ) + attributes: List[UserProfileAttribute] = Field( + default_factory=list, + description="User attributes", + ) + + class Config: + populate_by_name = True # Allow both field names and aliases + + +class GetUserProfileInput(BaseModel): + """Input for getting a user profile.""" + + schema_id: str = Field(..., description="Profile schema id") + user_id: str = Field(..., description="End user id") + + +class GetUserProfileOutput(BaseModel): + """Output from getting a user profile.""" + + request_id: str = Field(..., description="Request id", alias="requestId") + profile: UserProfile = Field(..., description="User profile") + + class Config: + populate_by_name = True # Allow both field names and aliases + From 6c6d8a8cc212c3e3c1c5bdb49f8fd9b99e4bd1c0 Mon Sep 17 00:00:00 2001 From: "taoquanyu.tqy" Date: Tue, 2 Dec 2025 19:28:49 +0800 Subject: [PATCH 3/4] refactor(memory): reformat code and improve error handling - Reformat Python files to adhere to consistent style guidelines - Improve error handling in ModelStudioMemoryBase by refining exception catching - Update configuration parsing to streamline environment variable usage - Adjust demo script formatting and enhance debugging output - Remove unused example component file - Modify model descriptions for clarity and conciseness --- examples/components/memory_with_llm.py | 203 ---------- examples/memory/memory_demo.py | 376 +++++++++++------- .../memory/modelstudio_memory/__init__.py | 1 - .../memory/modelstudio_memory/base.py | 25 +- .../memory/modelstudio_memory/config.py | 7 +- .../memory/modelstudio_memory/core.py | 63 +-- .../memory/modelstudio_memory/exceptions.py | 11 +- .../memory/modelstudio_memory/models.py | 3 +- 8 files changed, 292 insertions(+), 397 deletions(-) delete mode 100644 examples/components/memory_with_llm.py diff --git a/examples/components/memory_with_llm.py b/examples/components/memory_with_llm.py deleted file mode 100644 index 81ac0a8..0000000 --- a/examples/components/memory_with_llm.py +++ /dev/null @@ -1,203 +0,0 @@ -# -*- coding: utf-8 -*- -# type: ignore - -import asyncio -import os -import time -from typing import List - -from agentscope_bricks.components.memory.modelstudio_memory import ( - AddMemory, - SearchMemory, - ListMemory, - DeleteMemory, - AddMemoryInput, - SearchMemoryInput, - ListMemoryInput, - DeleteMemoryInput, - AddMemoryOutput, - SearchMemoryOutput, - ListMemoryOutput, - DeleteMemoryOutput, - Message, -) -from agentscope_bricks.models.llm import BaseLLM -from agentscope_bricks.utils.schemas.oai_llm import Parameters - -# ============= Configuration ============= -DASHSCOPE_API_KEY = os.getenv("DASHSCOPE_API_KEY", "") -# Configure the end user's ID to ensure their memory is isolated from others. -END_USER_ID = os.getenv("END_USER_ID", "default") - - -# Check required environment variables -if not DASHSCOPE_API_KEY: - raise ValueError("DASHSCOPE_API_KEY environment variable is not set") - -# ============= Component Initialization ============= -add_memory = AddMemory() -search_memory = SearchMemory() -list_memory = ListMemory() -delete_memory = DeleteMemory() -llm = BaseLLM() - - -# ============= Example Data ============= -def get_example_messages() -> List[Message]: - """Get example conversation messages for memory storage.""" - return [ - Message(role="user", content="每天上午11点提醒我点外卖。"), - Message(role="assistant", content="没问题"), - Message( - role="user", - content=[ - { - "type": "image_url", - "image_url": { - "url": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241022/emyrja/dog_and_girl.jpeg", # noqa E501 - }, - "description": "【标题】白板文字记录与会议纪要提醒\n【内容】白板内容涉及近代教育体系建立的相关知识点,包括维新派活动、清末新政、资产阶级革命派活动等。具体内容如下:\n- 维新派活动:(1)近代学制特点;(2)废科举、兴学堂的过程。\n- 清末新政:行政设立学部和提学使司。\n- 资产阶级革命派活动:爱国学社、中华革命党。\n- 近代学制特点:目的、系统性、内容、班级授课与学年制度、对待儿童方式、课程比重等。", # noqa E501 - }, - { - "type": "text", - "text": "记录一下白板这些文字,明天10点提醒我整理会议纪要。", - }, - ], - ), - Message(role="assistant", content="好的"), - ] - - -# ============= Memory Operations ============= -async def add_memory_example() -> AddMemoryOutput: - """Add conversation messages to memory.""" - return await add_memory.arun( - AddMemoryInput( - user_id=END_USER_ID, - messages=get_example_messages(), - source="rayneo", - timestamp=int(time.time()), - meta_data={ - "location_name": "北京", - "geo_coordinate": "116.481499,39.990475", - "media_desc": [ - { - "url": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241022/emyrja/dog_and_girl.jpeg", # noqa E501 - "description": "【标题】白板文字记录与会议纪要提醒\n【内容】白板内容涉及近代教育体系建立的相关知识点,包括维新派活动、清末新政、资产阶级革命派活动等。具体内容如下:\n- 维新派活动:(1)近代学制特点;(2)废科举、兴学堂的过程。\n- 清末新政:行政设立学部和提学使司。\n- 资产阶级革命派活动:爱国学社、中华革命党。\n- 近代学制特点:目的、系统性、内容、班级授课与学年制度、对待儿童方式、课程比重等。", # noqa E501 - }, - ], - }, - ), - ) - - -async def list_memory_example() -> ListMemoryOutput: - """List all memory nodes for a user.""" - return await list_memory.arun( - ListMemoryInput( - user_id=END_USER_ID, - page_num=1, - page_size=10, - ), - ) - - -async def search_memory_example(messages: List[Message]) -> SearchMemoryOutput: - """Search for relevant memories based on query.""" - return await search_memory.arun( - SearchMemoryInput( - user_id=END_USER_ID, - messages=messages, - top_k=5, - min_score=0, - ), - ) - - -async def delete_memory_example(memory_node_id: str) -> DeleteMemoryOutput: - """Delete a specific memory node.""" - return await delete_memory.arun( - DeleteMemoryInput( - user_id=END_USER_ID, - memory_node_id=memory_node_id, - ), - ) - - -# ============= LLM Integration ============= -async def get_llm_response( - search_result: SearchMemoryOutput, - user_query: str, -) -> None: - """Get LLM response based on retrieved memories.""" - system_prompt = f"""You are an experienced Assistant. Please answer the - question based on the retrieved memories. - -Retrieved memories: -{chr(10).join([f"- {node.content}" for node in search_result.memory_nodes])} -""" - - llm_messages = [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_query}, - ] - - parameters = Parameters( - stream=True, - stream_options={"include_usage": True}, - ) - async for chunk in llm.astream( - model="qwen-max", - messages=llm_messages, - parameters=parameters, - ): - print(chunk.choices[0].delta, end="\n", flush=True) - - -# ============= Main Execution ============= -async def main() -> None: - """Main execution function.""" - try: - # 1. Add memory - print("\n=== Adding Memory ===") - add_result = await add_memory_example() - print("Add Memory Result:", add_result) - - await asyncio.sleep(5) - # 2. Delete the newly added memory node - print("\n=== Deleting Memory ===") - if add_result.memory_nodes: - memory_node_id = add_result.memory_nodes[0].memory_node_id - if memory_node_id: - delete_result = await delete_memory_example(memory_node_id) - print("Delete Memory Result:", delete_result) - - await asyncio.sleep(5) - # 3. List memory - print("\n=== Listing Memory ===") - list_result = await list_memory_example() - print("List Memory Result:") - print(f"Request ID: {list_result.request_id}") - for node in list_result.memory_nodes: - print(f"Memory Node ID: {node.memory_node_id}") - print(f"Memory Node Content: {node.content}") - - await asyncio.sleep(5) - # 4. Search memory - user_query = "明天需要提醒我什么事?" - print("\n=== Searching Memory ===") - search_result = await search_memory_example( - [Message(role="user", content=user_query)], - ) - print("Search Memory Result:", search_result) - - # 5. Get LLM response - print("\n=== Getting LLM Response ===") - await get_llm_response(search_result, user_query=user_query) - - except Exception as e: - print("Error during execution:", e) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/memory/memory_demo.py b/examples/memory/memory_demo.py index d24d55c..af35883 100644 --- a/examples/memory/memory_demo.py +++ b/examples/memory/memory_demo.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import asyncio import logging import os @@ -30,23 +31,30 @@ # ===== 配置日志,过滤掉冗长的调试信息 ===== # 从环境变量读取日志级别,默认为 WARNING -LOG_LEVEL = os.getenv('LOG_LEVEL', 'WARNING').upper() +LOG_LEVEL = os.getenv("LOG_LEVEL", "WARNING").upper() logging.basicConfig( level=getattr(logging, LOG_LEVEL, logging.WARNING), - format='%(levelname)s: %(message)s' if LOG_LEVEL == 'WARNING' else '%(asctime)s [%(levelname)s] %(name)s: %(message)s' + format=( + "%(levelname)s: %(message)s" + if LOG_LEVEL == "WARNING" + else "%(asctime)s [%(levelname)s] %(name)s: %(message)s" + ), ) # 特别禁用某些组件的详细日志(除非明确设置为 DEBUG) -if LOG_LEVEL != 'DEBUG': - logging.getLogger('agentscope_bricks').setLevel(logging.WARNING) - logging.getLogger('httpx').setLevel(logging.WARNING) - logging.getLogger('httpcore').setLevel(logging.WARNING) +if LOG_LEVEL != "DEBUG": + logging.getLogger("agentscope_bricks").setLevel(logging.WARNING) + logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("httpcore").setLevel(logging.WARNING) def require_env(name: str) -> str: value = os.getenv(name) if not value: - print(f"[ERROR] Required environment variable not set: {name}", file=sys.stderr) + print( + f"[ERROR] Required environment variable not set: {name}", + file=sys.stderr, + ) sys.exit(1) return value @@ -88,28 +96,30 @@ def print_error(message: str) -> None: def format_api_error(error: MemoryAPIError) -> str: """格式化 API 错误信息以便显示""" parts = [] - + # 提取错误消息主体(不包括 __str__ 方法添加的额外信息) error_message = str(error).split(" | ")[0] parts.append(f"错误信息: {error_message}") - + if error.error_code: parts.append(f"错误代码: {error.error_code}") - + if error.status_code: parts.append(f"HTTP 状态码: {error.status_code}") - + if error.request_id: parts.append(f"Request ID: {error.request_id}") - + return "\n ".join(parts) -async def step_create_profile_schema(create_profile_schema: CreateProfileSchema) -> str: +async def step_create_profile_schema( + create_profile_schema: CreateProfileSchema, +) -> str: """创建用户画像 Schema""" print_info("用户画像 Schema 用于定义用户有哪些字段(如年龄、爱好)。") print("") - + payload = CreateProfileSchemaInput( name="用户画像(示例)", description="用于演示的用户基础画像 Schema", @@ -123,37 +133,47 @@ async def step_create_profile_schema(create_profile_schema: CreateProfileSchema) print_info("请求参数:") print_info(f" · Schema 名称:{payload.name}") print_info(f" · Schema 描述:{payload.description}") - print_info(f" · 字段定义:") + print_info(" · 字段定义:") for idx, attr in enumerate(payload.attributes, start=1): print_info(f" [{idx}] {attr.name} - {attr.description}") print("") result = await create_profile_schema.arun(payload) - print_success(f"✓ 已创建用户画像 Schema") + print_success("✓ 已创建用户画像 Schema") print_info(f" Schema ID:{result.profile_schema_id}") print_info(f" 请求ID:{result.request_id}") print("") - + return result.profile_schema_id def example_messages() -> List[Message]: return [ - Message(role="user", content="每天上午9点提醒我喝水,下午3点复习笔记。"), + Message( + role="user", + content="每天上午9点提醒我喝水,下午3点复习笔记。", + ), Message(role="assistant", content="好的,我已经记录下来。"), - Message(role="user", - content="还有明天记得提醒我给诺成老师买个生日礼物,诺成老师今年30岁了,比我大三岁。我们的爱好相同,经常一起踢球,所以我打算给诺成老师买一个精美的足球"), + Message( + role="user", + content="还有明天记得提醒我给诺成老师买个生日礼物,诺成老师今年30岁了,\ + 比我大三岁。我们的爱好相同,经常一起踢球,所以我打算给诺成老师买一个精美的足球", + ), Message(role="assistant", content="好的,我明天会提醒你"), ] -async def step_add_memory(add_memory: AddMemory, end_user_id: str, profile_schema_id: str) -> List[str]: +async def step_add_memory( + add_memory: AddMemory, + end_user_id: str, + profile_schema_id: str, +) -> List[str]: """添加对话记忆到记忆服务""" print_info("我们将一段对话提交到记忆服务,服务会自动完成两件事:") print_info(" 1️⃣ 抽取并保存记忆条目(memory nodes)") print_info(" 2️⃣ 从对话中提取用户画像信息(年龄、爱好等)") print("") - + now_ts = int(time.time()) msgs = example_messages() payload = AddMemoryInput( @@ -171,16 +191,26 @@ async def step_add_memory(add_memory: AddMemory, end_user_id: str, profile_schem print_info("📥 请求参数:") print_info(f" · 用户ID:{payload.user_id}") print_info(f" · Profile Schema ID:{truncate(profile_schema_id, 50)}") - print_info(f" · 时间戳:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(payload.timestamp))}") + + # 格式化时间戳 + timestamp_str = time.strftime( + "%Y-%m-%d %H:%M:%S", + time.localtime(payload.timestamp), + ) + print_info(f" · 时间戳:{timestamp_str}") print_info(f" · 对话消息数:{len(payload.messages)} 条") - print_info(f" · 元数据:位置={payload.meta_data.get('location_name')}, 坐标={payload.meta_data.get('geo_coordinate')}") + + # 格式化元数据 + location = payload.meta_data.get("location_name") + coordinate = payload.meta_data.get("geo_coordinate") + print_info(f" · 元数据:位置={location}, 坐标={coordinate}") print("") - + print_info("💬 对话内容(注意画像信息):") for idx, m in enumerate(payload.messages, start=1): role_icon = "👤" if m.role == "user" else "🤖" content_str = str(m.content) - + # 突出显示包含画像信息的对话 if "30岁" in content_str or "踢球" in content_str: print(f" {role_icon} [{m.role}] {truncate(content_str, 100)} 🎯") @@ -191,19 +221,25 @@ async def step_add_memory(add_memory: AddMemory, end_user_id: str, profile_schem print("") add_result = await add_memory.arun(payload) - + # 调试:打印返回结果类型 - print_info(f"🔍 调试信息:memory_nodes 类型 = {type(add_result.memory_nodes)}") - + print_info( + f"🔍 调试信息:memory_nodes 类型 = {type(add_result.memory_nodes)}", + ) + # 兼容处理:如果 memory_nodes 不是列表,转换为列表 if isinstance(add_result.memory_nodes, list): memory_nodes_list = add_result.memory_nodes else: # 如果是单个对象,包装成列表 - memory_nodes_list = [add_result.memory_nodes] if add_result.memory_nodes else [] - - node_ids = [n.memory_node_id for n in memory_nodes_list if n.memory_node_id] - + memory_nodes_list = ( + [add_result.memory_nodes] if add_result.memory_nodes else [] + ) + + node_ids = [ + n.memory_node_id for n in memory_nodes_list if n.memory_node_id + ] + if node_ids: print_success(f"✓ 成功新增 {len(node_ids)} 条记忆条目") print_info(f" 请求ID:{add_result.request_id}") @@ -218,16 +254,25 @@ async def step_add_memory(add_memory: AddMemory, end_user_id: str, profile_schem print("") else: print_warn("⚠ 未返回任何记忆条目 ID,稍后删除步骤将跳过。") - + return node_ids -async def step_list_memory(list_memory: ListMemory, end_user_id: str, page_num: int = 1, page_size: int = 10) -> List[str]: +async def step_list_memory( + list_memory: ListMemory, + end_user_id: str, + page_num: int = 1, + page_size: int = 10, +) -> List[str]: """列出用户的所有记忆条目(分页)""" print_info("列出该用户当前保存的所有记忆条目(分页查询)。") print("") - - payload = ListMemoryInput(user_id=end_user_id, page_num=page_num, page_size=page_size) + + payload = ListMemoryInput( + user_id=end_user_id, + page_num=page_num, + page_size=page_size, + ) # 展示示例参数 print_info("请求参数:") @@ -237,19 +282,27 @@ async def step_list_memory(list_memory: ListMemory, end_user_id: str, page_num: print("") result = await list_memory.arun(payload) - total_pages = (result.total + result.page_size - 1) // result.page_size if result.page_size else 1 - + total_pages = ( + (result.total + result.page_size - 1) // result.page_size + if result.page_size + else 1 + ) + print_success(f"✓ 列表获取成功 (请求ID: {result.request_id})") - print_info(f"📊 分页信息:第 {result.page_num}/{total_pages} 页,每页 {result.page_size} 条,共 {result.total} 条") + print_info( + f"📊 分页信息:第 \ + {result.page_num}/{total_pages} 页,\ + 每页 {result.page_size} 条,共 {result.total} 条", + ) print("") - + if not result.memory_nodes: print_info("(当前页无记忆条目)") return [] - + print_info(f"📝 记忆条目列表(当前页共 {len(result.memory_nodes)} 条):") print("") - + existing_ids = [] for idx, node in enumerate(result.memory_nodes, start=1): existing_ids.append(node.memory_node_id or "") @@ -257,18 +310,24 @@ async def step_list_memory(list_memory: ListMemory, end_user_id: str, page_num: print(f" ID: {node.memory_node_id}") if idx < len(result.memory_nodes): print("") - + print("") return [nid for nid in existing_ids if nid] -async def step_search_memory_with_llm(search_memory: SearchMemory, llm: BaseLLM, end_user_id: str) -> Tuple[List[str], str]: +async def step_search_memory_with_llm( + search_memory: SearchMemory, + llm: BaseLLM, + end_user_id: str, +) -> Tuple[List[str], str]: """检索记忆并使用大模型生成个性化回答""" user_query = "今天和明天需要提醒我做什么?" - - print_info("我们将用一个自然语言问题来检索相关记忆,然后让大模型基于这些记忆生成个性化回答。") + + print_info( + "我们将用一个自然语言问题来检索相关记忆,然后让大模型基于这些记忆生成个性化回答。", + ) print("") - + # 1. 检索记忆 print_info("🔍 第一步:检索相关记忆") payload = SearchMemoryInput( @@ -287,29 +346,31 @@ async def step_search_memory_with_llm(search_memory: SearchMemory, llm: BaseLLM, search_result = await search_memory.arun(payload) print_success(f"✓ 检索完成 (请求ID: {search_result.request_id})") - + if not search_result.memory_nodes: print_warn("未找到相关记忆条目") return [], user_query - + print_info(f"找到 {len(search_result.memory_nodes)} 条相关记忆:") print("") - + hit_ids = [] for idx, node in enumerate(search_result.memory_nodes, start=1): hit_ids.append(node.memory_node_id or "") print(f" [{idx}] {truncate(node.content, 100)}") print(f" ID: {node.memory_node_id}") - + print("") print("─" * 70) print("") - + # 2. 使用大模型生成回答 print_info("🤖 第二步:基于检索到的记忆,让大模型生成个性化回答") print("") - - context_lines = [f"- {node.content}" for node in search_result.memory_nodes] + + context_lines = [ + f"- {node.content}" for node in search_result.memory_nodes + ] system_prompt = ( "你是一名助理。根据以下检索到的记忆回答用户问题。\n\n" + "记忆内容:\n" @@ -323,47 +384,46 @@ async def step_search_memory_with_llm(search_memory: SearchMemory, llm: BaseLLM, params = Parameters(stream=True, stream_options={"include_usage": True}) model_name = "qwen-max" - + print_info(f"模型:{model_name}(流式输出)") print_info(f"问题:{user_query}") print("") print_success("模型回答:") - print("┌" + "─" * 68 + "┐") - print("│", end=" ") - - char_count = 0 - async for chunk in llm.astream(model=model_name, messages=messages, parameters=params): + print("") + print(" ", end="") + + async for chunk in llm.astream( + model=model_name, + messages=messages, + parameters=params, + ): if chunk.choices: delta = getattr(chunk.choices[0], "delta", None) if delta is not None: text = str(delta) print(text, end="", flush=True) - char_count += len(text) - # 每70个字符换行并添加边框 - if char_count >= 66: - print(" │") - print("│", end=" ") - char_count = 0 - - if char_count > 0: - # 补齐最后一行 - padding = " " * (67 - char_count) - print(f"{padding}│") - print("└" + "─" * 68 + "┘") + + print("") print("") - + return [hid for hid in hit_ids if hid], user_query -async def step_get_user_profile(get_user_profile: GetUserProfile, schema_id: str, end_user_id: str) -> None: +async def step_get_user_profile( + get_user_profile: GetUserProfile, + schema_id: str, + end_user_id: str, +) -> None: """获取并展示用户画像信息""" print_info("🎯 用户画像功能展示") print("") - print_info("💡 说明:记忆服务会自动从对话中提取用户信息,填充到画像字段中。") + print_info( + "💡 说明:记忆服务会自动从对话中提取用户信息,填充到画像字段中。", + ) print_info(" 例如:从 '诺成老师今年30岁,比我大三岁' 可推断出用户27岁") print_info(" 从 '我们经常一起踢球' 可推断出用户爱好是足球") print("") - + payload = GetUserProfileInput(schema_id=schema_id, user_id=end_user_id) # 展示示例参数 @@ -375,70 +435,60 @@ async def step_get_user_profile(get_user_profile: GetUserProfile, schema_id: str result = await get_user_profile.arun(payload) print_success(f"✓ 已获取用户画像 (请求ID: {result.request_id})") print("") - + # 显示 Schema 信息 - print("┌" + "─" * 68 + "┐") - print(f"│ 📋 Schema 信息{' ' * 52}│") - print("├" + "─" * 68 + "┤") + print_info("📋 Schema 信息:") schema_name = result.profile.schema_name or "(未设置)" schema_desc = result.profile.schema_description or "(未设置)" - print(f"│ 名称: {schema_name:<60}│") - print(f"│ 描述: {schema_desc:<60}│") - print("└" + "─" * 68 + "┘") + print_info(f" 名称: {schema_name}") + print_info(f" 描述: {schema_desc}") print("") - + # 显示用户画像 if result.profile.attributes: - print("┌" + "─" * 68 + "┐") - print(f"│ 👤 用户画像(共 {len(result.profile.attributes)} 个字段){' ' * (49 - len(str(len(result.profile.attributes))))}│") - print("├" + "─" * 68 + "┤") - + print_info( + f"👤 用户画像(共 {len(result.profile.attributes)} 个字段):", + ) + print("") + for idx, attr in enumerate(result.profile.attributes, start=1): value_display = attr.value if attr.value else "(暂未提取)" - - # 字段名 - print(f"│ [{idx}] {attr.name:<61}│") - - # 字段值(可能很长,需要处理) - if len(str(value_display)) <= 60: - print(f"│ 值: {str(value_display):<58}│") - else: - # 长文本分行显示 - value_str = str(value_display) - print(f"│ 值: {value_str[:58]:<58}│") - for i in range(58, len(value_str), 62): - line = value_str[i:i+62] - print(f"│ {line:<62}│") - - # 字段ID - id_display = truncate(attr.id, 58) - print(f"│ ID: {id_display:<58}│") - + + print_info(f" [{idx}] {attr.name}") + print_info(f" 值: {value_display}") + print_info(f" ID: {attr.id}") + # 分隔线(最后一个除外) if idx < len(result.profile.attributes): - print("│" + " " * 68 + "│") - - print("└" + "─" * 68 + "┘") + print("") + print("") - + # 如果有字段被填充,添加说明 has_values = any(attr.value for attr in result.profile.attributes) if has_values: - print_success("💡 提示:上述画像信息是记忆服务自动从对话中提取的!") - print("") + print_success( + "💡 提示:上述画像信息是记忆服务自动从对话中提取的!", + ) else: - print_info("💡 提示:画像字段暂未填充,随着更多对话的积累,会逐步完善。") - print("") + print_info( + "💡 提示:画像字段暂未填充,随着更多对话的积累,会逐步完善。", + ) + print("") else: print_info("(暂无画像字段)") print("") -async def step_delete_memory(delete_memory: DeleteMemory, end_user_id: str, node_ids: List[str]) -> None: +async def step_delete_memory( + delete_memory: DeleteMemory, + end_user_id: str, + node_ids: List[str], +) -> None: """删除指定的记忆条目""" print_info("删除刚才新增的记忆条目,演示数据清理功能。") print("") - + if not node_ids: print_warn("⚠ 没有可删除的条目,跳过该步骤。") return @@ -451,20 +501,20 @@ async def step_delete_memory(delete_memory: DeleteMemory, end_user_id: str, node print_info(f"🗑️ 正在删除 {len(node_ids)} 条记忆...") print("") - + for idx, node_id in enumerate(node_ids, start=1): result = await delete_memory.arun( - DeleteMemoryInput(user_id=end_user_id, memory_node_id=node_id) + DeleteMemoryInput(user_id=end_user_id, memory_node_id=node_id), + ) + print_success( + f" ✓ [{idx}/{len(node_ids)}] 已删除:{truncate(node_id, 50)}", ) - print_success(f" ✓ [{idx}/{len(node_ids)}] 已删除:{truncate(node_id, 50)}") print_info(f" 请求ID:{result.request_id}") - + print("") print_success(f"✓ 全部删除完成,共删除 {len(node_ids)} 条记忆") - - async def main() -> None: # Required envs require_env("DASHSCOPE_API_KEY") @@ -483,19 +533,35 @@ async def main() -> None: print_section("Demo 0: Create Profile Schema") try: schema_id = await step_create_profile_schema(create_profile_schema) - except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError) as e: + except ( + MemoryAPIError, + MemoryAuthenticationError, + MemoryValidationError, + ) as e: print_error("❌ 创建用户画像 Schema 失败:") print_error(f" {format_api_error(e)}") - print_error("\n💡 建议:请检查 API Key 是否正确,或查看 Request ID 联系技术支持") + print_error( + "\n💡 建议:请检查 API Key 是否正确,或查看 Request ID 联系技术支持", + ) return print_section("Demo 1: Add Memory") try: - node_ids = await step_add_memory(add_memory, end_user_id, schema_id) - except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError) as e: + node_ids = await step_add_memory( + add_memory, + end_user_id, + schema_id, + ) + except ( + MemoryAPIError, + MemoryAuthenticationError, + MemoryValidationError, + ) as e: print_error("❌ 添加记忆失败:") print_error(f" {format_api_error(e)}") - print_error("\n💡 建议:请检查参数是否正确,或查看 Request ID 联系技术支持") + print_error( + "\n💡 建议:请检查参数是否正确,或查看 Request ID 联系技术支持", + ) return # Wait for consistency @@ -507,17 +573,28 @@ async def main() -> None: # 2. List memory print_section("Demo 2: List Memory") try: - existing_ids = await step_list_memory(list_memory, end_user_id) - except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError) as e: + await step_list_memory(list_memory, end_user_id) + except ( + MemoryAPIError, + MemoryAuthenticationError, + MemoryValidationError, + ) as e: print_error("❌ 列出记忆失败:") print_error(f" {format_api_error(e)}") # 非关键步骤,可以继续 - existing_ids = [] print_section("Demo 3: Search Memory + LLM Answer") try: - _hits, _query = await step_search_memory_with_llm(search_memory, llm, end_user_id) - except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError) as e: + _hits, _query = await step_search_memory_with_llm( + search_memory, + llm, + end_user_id, + ) + except ( + MemoryAPIError, + MemoryAuthenticationError, + MemoryValidationError, + ) as e: print_error("❌ 搜索记忆失败:") print_error(f" {format_api_error(e)}") # 非关键步骤,可以继续 @@ -531,8 +608,17 @@ async def main() -> None: print_section("Demo 4: Get User Profile (展示自动提取的用户画像)") try: - await step_get_user_profile(get_user_profile, schema_id, end_user_id) - except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError, MemoryNotFoundError) as e: + await step_get_user_profile( + get_user_profile, + schema_id, + end_user_id, + ) + except ( + MemoryAPIError, + MemoryAuthenticationError, + MemoryValidationError, + MemoryNotFoundError, + ) as e: print_error("❌ 获取用户画像失败:") print_error(f" {format_api_error(e)}") # 非关键步骤,可以继续 @@ -540,7 +626,11 @@ async def main() -> None: print_section("Demo 5: Delete Memory") try: await step_delete_memory(delete_memory, end_user_id, node_ids) - except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError) as e: + except ( + MemoryAPIError, + MemoryAuthenticationError, + MemoryValidationError, + ) as e: print_error("❌ 删除记忆失败:") print_error(f" {format_api_error(e)}") # 非关键步骤,可以继续 @@ -554,15 +644,19 @@ async def main() -> None: print_section("Demo 6: List Memory Again (验证删除)") try: await step_list_memory(list_memory, end_user_id) - except (MemoryAPIError, MemoryAuthenticationError, MemoryValidationError) as e: + except ( + MemoryAPIError, + MemoryAuthenticationError, + MemoryValidationError, + ) as e: print_error("❌ 列出记忆失败:") print_error(f" {format_api_error(e)}") - + print("") print("=" * 70) print_success("🎉 所有演示步骤已完成!") print("=" * 70) - + finally: # 清理资源:关闭所有 HTTP 连接 print("") @@ -578,5 +672,3 @@ async def main() -> None: if __name__ == "__main__": asyncio.run(main()) - - diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory/__init__.py b/src/agentscope_bricks/components/memory/modelstudio_memory/__init__.py index a46d30b..d226f2c 100644 --- a/src/agentscope_bricks/components/memory/modelstudio_memory/__init__.py +++ b/src/agentscope_bricks/components/memory/modelstudio_memory/__init__.py @@ -103,4 +103,3 @@ "GetUserProfileInput", "GetUserProfileOutput", ] - diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory/base.py b/src/agentscope_bricks/components/memory/modelstudio_memory/base.py index c4fed84..ab04634 100644 --- a/src/agentscope_bricks/components/memory/modelstudio_memory/base.py +++ b/src/agentscope_bricks/components/memory/modelstudio_memory/base.py @@ -3,11 +3,14 @@ Base class for ModelStudio Memory components. """ import logging -from typing import Any, Dict, Optional +from types import TracebackType +from typing import Any, Dict, Optional, Type import aiohttp -from agentscope_bricks.components.memory.modelstudio_memory.config import MemoryServiceConfig +from agentscope_bricks.components.memory.modelstudio_memory.config import ( + MemoryServiceConfig, +) from agentscope_bricks.components.memory.modelstudio_memory.exceptions import ( MemoryAPIError, MemoryAuthenticationError, @@ -112,7 +115,7 @@ async def _request( f"Request successful: {method} {url}", ) return result - + # Handle error responses (4XX, 5XX) # Try to parse JSON error response first error_data = None @@ -122,12 +125,12 @@ async def _request( # If JSON parsing fails, fall back to text error_text = await response.text() error_data = {"message": error_text} - + # Extract error information error_code = error_data.get("code", "Unknown") error_message = error_data.get("message", "Unknown error") request_id = error_data.get("request_id", "") - + # Format error log error_log = ( f"API Error - Status: {response.status}, " @@ -135,7 +138,7 @@ async def _request( f"Request ID: {request_id}" ) logger.error(error_log) - + # Raise appropriate exception based on status code if response.status == 401 or response.status == 403: raise MemoryAuthenticationError( @@ -205,11 +208,15 @@ async def close(self) -> None: await self._session.close() logger.debug("Session closed") - async def __aenter__(self): + async def __aenter__(self) -> "ModelStudioMemoryBase": """Support async context manager.""" return self - async def __aexit__(self, exc_type, exc_val, exc_tb): + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: """Support async context manager.""" await self.close() - diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory/config.py b/src/agentscope_bricks/components/memory/modelstudio_memory/config.py index c7321de..f734b61 100644 --- a/src/agentscope_bricks/components/memory/modelstudio_memory/config.py +++ b/src/agentscope_bricks/components/memory/modelstudio_memory/config.py @@ -47,7 +47,7 @@ def from_env(cls) -> "MemoryServiceConfig": if not api_key: raise ValueError( "DASHSCOPE_API_KEY environment variable is required. " - "Please set it before using ModelStudio Memory components." + "Please set it before using ModelStudio Memory components.", ) return cls( @@ -81,5 +81,6 @@ def get_create_profile_schema_url(self) -> str: def get_user_profile_url(self, schema_id: str) -> str: """Get URL for getting user profile.""" - return f"{self.service_endpoint}/profile_schemas/{schema_id}/user_profile" - + return ( + f"{self.service_endpoint}/profile_schemas/{schema_id}/user_profile" + ) diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory/core.py b/src/agentscope_bricks/components/memory/modelstudio_memory/core.py index ab05771..3e1a1f2 100644 --- a/src/agentscope_bricks/components/memory/modelstudio_memory/core.py +++ b/src/agentscope_bricks/components/memory/modelstudio_memory/core.py @@ -46,7 +46,7 @@ class AddMemory( """ Component for storing conversation history as memory nodes. - This component sends conversation messages to the ModelStudio Memory service + This component sends conversation messages to the ModelStudio Memory to be processed and stored as searchable memory nodes. The service automatically extracts and structures relevant information. @@ -79,9 +79,9 @@ def __init__(self, config: Optional[MemoryServiceConfig] = None) -> None: ModelStudioMemoryBase.__init__(self, config) async def _arun( - self, - args: AddMemoryInput, - **kwargs: Any, + self, + args: AddMemoryInput, + **kwargs: Any, ) -> AddMemoryOutput: """ Add memory nodes for the given conversation. @@ -112,9 +112,11 @@ async def _arun( # Debug: print API response structure logger.debug(f"API Response: {result}") - logger.debug(f"memory_nodes type: {type(result.get('memory_nodes'))}") + logger.debug( + f"memory_nodes type: {type(result.get('memory_nodes'))}", + ) logger.debug(f"memory_nodes value: {result.get('memory_nodes')}") - + # Parse response - handle both list and dict formats memory_nodes_raw = result.get("memory_nodes", []) if isinstance(memory_nodes_raw, dict): @@ -124,11 +126,10 @@ async def _arun( memory_nodes_list = memory_nodes_raw else: memory_nodes_list = [] - + output = AddMemoryOutput( memory_nodes=[ - MemoryNode(**node) - for node in memory_nodes_list + MemoryNode(**node) for node in memory_nodes_list ], request_id=result.get("request_id", ""), ) @@ -138,7 +139,7 @@ async def _arun( ) return output - except Exception as e: + except Exception: logger.exception(f"Failed to add memory for user {args.user_id}") raise @@ -178,9 +179,9 @@ def __init__(self, config: Optional[MemoryServiceConfig] = None) -> None: ModelStudioMemoryBase.__init__(self, config) async def _arun( - self, - args: SearchMemoryInput, - **kwargs: Any, + self, + args: SearchMemoryInput, + **kwargs: Any, ) -> SearchMemoryOutput: """ Search for relevant memory nodes. @@ -226,7 +227,7 @@ async def _arun( ) return output - except Exception as e: + except Exception: logger.exception( f"Failed to search memory for user {args.user_id}", ) @@ -268,9 +269,9 @@ def __init__(self, config: Optional[MemoryServiceConfig] = None) -> None: ModelStudioMemoryBase.__init__(self, config) async def _arun( - self, - args: ListMemoryInput, - **kwargs: Any, + self, + args: ListMemoryInput, + **kwargs: Any, ) -> ListMemoryOutput: """ List memory nodes for a user with pagination. @@ -320,7 +321,7 @@ async def _arun( ) return output - except Exception as e: + except Exception: logger.exception(f"Failed to list memory for user {args.user_id}") raise @@ -360,9 +361,9 @@ def __init__(self, config: Optional[MemoryServiceConfig] = None) -> None: ModelStudioMemoryBase.__init__(self, config) async def _arun( - self, - args: DeleteMemoryInput, - **kwargs: Any, + self, + args: DeleteMemoryInput, + **kwargs: Any, ) -> DeleteMemoryOutput: """ Delete a memory node. @@ -400,7 +401,7 @@ async def _arun( ) return output - except Exception as e: + except Exception: logger.exception( f"Failed to delete memory node {args.memory_node_id}", ) @@ -414,7 +415,7 @@ class CreateProfileSchema( """ Component for creating a user profile schema. - This component creates a schema that defines the structure of user profiles, + This component creates a schema that defines the structure of user profiles including attribute definitions. Environment Variables: @@ -443,9 +444,9 @@ def __init__(self, config: Optional[MemoryServiceConfig] = None) -> None: ModelStudioMemoryBase.__init__(self, config) async def _arun( - self, - args: CreateProfileSchemaInput, - **kwargs: Any, + self, + args: CreateProfileSchemaInput, + **kwargs: Any, ) -> CreateProfileSchemaOutput: """ Create a profile schema. @@ -486,7 +487,7 @@ async def _arun( ) return output - except Exception as e: + except Exception: logger.exception(f"Failed to create profile schema: {args.name}") raise @@ -526,9 +527,9 @@ def __init__(self, config: Optional[MemoryServiceConfig] = None) -> None: ModelStudioMemoryBase.__init__(self, config) async def _arun( - self, - args: GetUserProfileInput, - **kwargs: Any, + self, + args: GetUserProfileInput, + **kwargs: Any, ) -> GetUserProfileOutput: """ Get a user profile. @@ -587,7 +588,7 @@ async def _arun( ) return output - except Exception as e: + except Exception: logger.exception( f"Failed to get profile for user {args.user_id}", ) diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory/exceptions.py b/src/agentscope_bricks/components/memory/modelstudio_memory/exceptions.py index 1a124c6..369d2e3 100644 --- a/src/agentscope_bricks/components/memory/modelstudio_memory/exceptions.py +++ b/src/agentscope_bricks/components/memory/modelstudio_memory/exceptions.py @@ -8,7 +8,7 @@ class MemoryAPIError(Exception): """ Base exception for Memory API errors. - + Attributes: message: Error message status_code: HTTP status code @@ -31,16 +31,16 @@ def __init__( def __str__(self) -> str: """Format error message with all available information.""" parts = [super().__str__()] - + if self.error_code: parts.append(f"Error Code: {self.error_code}") - + if self.status_code: parts.append(f"Status Code: {self.status_code}") - + if self.request_id: parts.append(f"Request ID: {self.request_id}") - + return " | ".join(parts) @@ -66,4 +66,3 @@ class MemoryNetworkError(MemoryAPIError): """Raised when network communication fails.""" pass - diff --git a/src/agentscope_bricks/components/memory/modelstudio_memory/models.py b/src/agentscope_bricks/components/memory/modelstudio_memory/models.py index a642d4b..660227a 100644 --- a/src/agentscope_bricks/components/memory/modelstudio_memory/models.py +++ b/src/agentscope_bricks/components/memory/modelstudio_memory/models.py @@ -38,7 +38,7 @@ class AddMemoryInput(BaseModel): timestamp: int = Field(..., description="Timestamp of the memory") meta_data: Optional[Dict[str, Any]] = Field( None, - description="Optional metadata including location and media description", + description="Optional metadata", ) class Config: @@ -243,4 +243,3 @@ class GetUserProfileOutput(BaseModel): class Config: populate_by_name = True # Allow both field names and aliases - From a6b7916af12f04f0400b3c1ef8e9a27a2f9ffdba Mon Sep 17 00:00:00 2001 From: "taoquanyu.tqy" Date: Tue, 2 Dec 2025 19:30:44 +0800 Subject: [PATCH 4/4] fix(memory): adjust content formatting in memory demo - Removed extra backslash escape characters in user message content - Improved line continuation formatting for better readability - Maintained same conversational context and information structure --- examples/memory/memory_demo.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/memory/memory_demo.py b/examples/memory/memory_demo.py index af35883..7e93583 100644 --- a/examples/memory/memory_demo.py +++ b/examples/memory/memory_demo.py @@ -156,8 +156,9 @@ def example_messages() -> List[Message]: Message(role="assistant", content="好的,我已经记录下来。"), Message( role="user", - content="还有明天记得提醒我给诺成老师买个生日礼物,诺成老师今年30岁了,\ - 比我大三岁。我们的爱好相同,经常一起踢球,所以我打算给诺成老师买一个精美的足球", + content="还有明天记得提醒我给诺成老师买个生日礼物,\ + 诺成老师今年30岁了,比我大三岁。我们的爱好相同,\ + 经常一起踢球,所以我打算给诺成老师买一个精美的足球", ), Message(role="assistant", content="好的,我明天会提醒你"), ]