From f8202ce58ef1a1c37151fdeb7a8fd95b9738fe28 Mon Sep 17 00:00:00 2001 From: RadeenXALNW Date: Sat, 29 Nov 2025 01:59:47 +0000 Subject: [PATCH 1/8] adding persistence in serverless --- examples/agno_persistent/agent.py | 166 ++++++++++++++++++ examples/agno_persistent/requirements.txt | 2 + examples/agno_persistent/runagent.config.json | 33 ++++ .../agent/runagent.config.json | 10 +- .../dart_sdk/.dart_tool/package_graph.json | 58 +++--- .../journalist_agent/dart_sdk/lib/main.dart | 4 +- runagent/client/client.py | 32 +++- runagent/sdk/deployment/remote.py | 37 +++- runagent/sdk/rest_client.py | 21 ++- runagent/sdk/sdk.py | 12 ++ runagent/sdk/socket_client.py | 14 +- runagent/utils/schema.py | 7 + .../python/client_test_agno_persistent.py | 28 +++ 13 files changed, 380 insertions(+), 44 deletions(-) create mode 100644 examples/agno_persistent/agent.py create mode 100644 examples/agno_persistent/requirements.txt create mode 100644 examples/agno_persistent/runagent.config.json create mode 100644 test_scripts/python/client_test_agno_persistent.py diff --git a/examples/agno_persistent/agent.py b/examples/agno_persistent/agent.py new file mode 100644 index 0000000..108914d --- /dev/null +++ b/examples/agno_persistent/agent.py @@ -0,0 +1,166 @@ +from textwrap import dedent +from typing import List, Optional +from agno.agent import Agent +from agno.db.base import SessionType +from agno.db.sqlite import SqliteDb +from agno.models.openai import OpenAIChat +from agno.session import AgentSession + +# Single SQLite DB for all agents/sessions +# Note: The backend automatically maps "rad" folder to /persistent/rad +# So we can use relative path "rad/agents.db" and it will persist across VM restarts +db = SqliteDb(db_file="rad/agents.db") + + +def get_chat_agent( + user: str = "user", + new_session: bool = True, + session_id: Optional[str] = None, +) -> Agent: + """ + Create and return a simple persistent chatbot Agent. + + Args: + user: User ID to associate with the agent. + new_session: If False, will try to resume the latest existing session + for this user (unless session_id is provided). + session_id: If provided and new_session=False, use this specific session. + + Returns: + An initialized Agent instance. + """ + resolved_session_id: Optional[str] = None + + if not new_session: + # If caller gave a specific session_id, use it + if session_id is not None: + resolved_session_id = session_id + else: + # Otherwise, try to resume the latest existing session for this user + existing_sessions: List[AgentSession] = db.get_sessions( # type: ignore + user_id=user, + session_type=SessionType.AGENT, + ) + if len(existing_sessions) > 0: + resolved_session_id = existing_sessions[0].session_id + + agent = Agent( + user_id=user, + session_id=resolved_session_id, + model=OpenAIChat(id="gpt-4o-mini"), + instructions=dedent( + """\ + You are a helpful, general-purpose AI assistant. + + Goals: + - Answer questions clearly and concisely. + - Ask for clarification when needed. + - Use a friendly, professional tone. + + You have access to the conversation history via the session. + Use that context to keep multi-turn conversations coherent. + """ + ), + db=db, + # Persist and reuse chat history across runs for this session + read_chat_history=True, + markdown=True, + ) + + return agent + + +def agent_print_response(prompt: str = None, user: str = "user", session_id: Optional[str] = None, new_session: bool = False, **kwargs): + """ + Non-streaming response with session management. + + Args: + prompt: User's message/prompt (can also be passed as 'message' in kwargs) + user: User ID for session management (default: "user") + session_id: Optional specific session ID to resume (default: None, will auto-resume latest) + new_session: If True, creates a new session. If False, resumes existing session (default: False) + **kwargs: Additional parameters (supports 'message' as alias for 'prompt') + + Returns: + Serializable response content with session metadata + """ + # Handle prompt from kwargs if not provided directly + if prompt is None: + prompt = kwargs.get('message', '') + + agent = get_chat_agent(user=user, new_session=new_session, session_id=session_id) + + # Get the response object + response = agent.run(prompt) + + # Extract the actual content from the response object + if hasattr(response, 'content'): + return { + "content": response.content, + "session_id": agent.session_id, + "user_id": agent.user_id + } + elif hasattr(response, 'messages') and response.messages: + # Get the last message content + content = response.messages[-1].content if hasattr(response.messages[-1], 'content') else str(response.messages[-1]) + return { + "content": content, + "session_id": agent.session_id, + "user_id": agent.user_id + } + elif hasattr(response, 'text'): + return { + "content": response.text, + "session_id": agent.session_id, + "user_id": agent.user_id + } + else: + # Fallback: convert to string + return { + "content": str(response), + "session_id": agent.session_id, + "user_id": agent.user_id + } + + +def agent_print_response_stream(prompt: str = None, user: str = "user", session_id: Optional[str] = None, new_session: bool = False, **kwargs): + """ + Streaming response with session management. + + Args: + prompt: User's message/prompt (can also be passed as 'message' in kwargs) + user: User ID for session management (default: "user") + session_id: Optional specific session ID to resume (default: None, will auto-resume latest) + new_session: If True, creates a new session. If False, resumes existing session (default: False) + **kwargs: Additional parameters (supports 'message' as alias for 'prompt') + + Yields: + Serializable chunks with content and session metadata + """ + # Handle prompt from kwargs if not provided directly + if prompt is None: + prompt = kwargs.get('message', '') + + agent = get_chat_agent(user=user, new_session=new_session, session_id=session_id) + + # Yield session info first + yield { + "type": "session_info", + "session_id": agent.session_id, + "user_id": agent.user_id + } + + # Stream the response + for chunk in agent.run(prompt, stream=True): + yield { + "type": "content", + "content": chunk.content if hasattr(chunk, 'content') else str(chunk) + } + + # Yield final session confirmation + yield { + "type": "session_end", + "session_id": agent.session_id + } + + diff --git a/examples/agno_persistent/requirements.txt b/examples/agno_persistent/requirements.txt new file mode 100644 index 0000000..832f552 --- /dev/null +++ b/examples/agno_persistent/requirements.txt @@ -0,0 +1,2 @@ +agno +openai \ No newline at end of file diff --git a/examples/agno_persistent/runagent.config.json b/examples/agno_persistent/runagent.config.json new file mode 100644 index 0000000..80fe7c5 --- /dev/null +++ b/examples/agno_persistent/runagent.config.json @@ -0,0 +1,33 @@ +{ + "agent_name": "Agno Memory", + "description": "My AI agent", + "framework": "default", + "template": "", + "version": "1.0.0", + "created_at": "2025-11-28T20:59:22.479436", + "template_source": { + "repo_url": "https://github.com/runagent-dev/runagent.git", + "author": "runagent-cli", + "path": "/home/azureuser/runagent/examples/agno_persistent" + }, + "agent_architecture": { + "entrypoints": [ + { + "file": "agent.py", + "module": "agent_print_response", + "tag":"agno_print_response" + }, + { + "file": "agent.py", + "module": "agent_print_response_stream", + "tag":"agno_print_response_stream" + } + ] + }, + "env_vars": {}, + "agent_id": "c998c025-4c9f-4466-886e-94345efe654b", + "auth_settings": { + "type": "api_key" + }, + "persistent_folders": ["rad"] +} \ No newline at end of file diff --git a/examples/journalist_agent/agent/runagent.config.json b/examples/journalist_agent/agent/runagent.config.json index 098a392..cfa56cc 100644 --- a/examples/journalist_agent/agent/runagent.config.json +++ b/examples/journalist_agent/agent/runagent.config.json @@ -25,8 +25,14 @@ ] }, "env_vars": {}, - "agent_id": "9fac4988-d88e-4d6c-994c-7495c11de8b9", + "agent_id": "9fac4988-d88e-4d6c-994c-7495c14de8b9", "auth_settings": { "type": "api_key" - } + }, + "persistent_folders": [ + ".network", + ".sqlite", + "data", + "embeddings" + ] } \ No newline at end of file diff --git a/examples/journalist_agent/dart_sdk/.dart_tool/package_graph.json b/examples/journalist_agent/dart_sdk/.dart_tool/package_graph.json index d4c9016..eb530c7 100644 --- a/examples/journalist_agent/dart_sdk/.dart_tool/package_graph.json +++ b/examples/journalist_agent/dart_sdk/.dart_tool/package_graph.json @@ -13,7 +13,7 @@ }, { "name": "runagent", - "version": "0.1.0", + "version": "0.1.41", "dependencies": [ "http", "web_socket_channel" @@ -29,6 +29,16 @@ "web" ] }, + { + "name": "http", + "version": "1.6.0", + "dependencies": [ + "async", + "http_parser", + "meta", + "web" + ] + }, { "name": "web", "version": "0.5.1", @@ -49,27 +59,18 @@ ] }, { - "name": "typed_data", - "version": "1.4.0", + "name": "async", + "version": "2.13.0", "dependencies": [ - "collection" + "collection", + "meta" ] }, { - "name": "collection", - "version": "1.19.1", + "name": "meta", + "version": "1.17.0", "dependencies": [] }, - { - "name": "http", - "version": "1.6.0", - "dependencies": [ - "async", - "http_parser", - "meta", - "web" - ] - }, { "name": "http_parser", "version": "4.1.2", @@ -80,6 +81,18 @@ "typed_data" ] }, + { + "name": "typed_data", + "version": "1.4.0", + "dependencies": [ + "collection" + ] + }, + { + "name": "collection", + "version": "1.19.1", + "dependencies": [] + }, { "name": "string_scanner", "version": "1.4.1", @@ -105,19 +118,6 @@ "name": "path", "version": "1.9.1", "dependencies": [] - }, - { - "name": "async", - "version": "2.13.0", - "dependencies": [ - "collection", - "meta" - ] - }, - { - "name": "meta", - "version": "1.17.0", - "dependencies": [] } ], "configVersion": 1 diff --git a/examples/journalist_agent/dart_sdk/lib/main.dart b/examples/journalist_agent/dart_sdk/lib/main.dart index a3f6873..fb2bfe9 100644 --- a/examples/journalist_agent/dart_sdk/lib/main.dart +++ b/examples/journalist_agent/dart_sdk/lib/main.dart @@ -5,13 +5,13 @@ Future main() async { // Create client (update agent_id after deployment) final client = await RunAgentClient.create( RunAgentClientConfig.create( - agentId: '9fac4988-d88e-4d6c-994c-7495c11de8b9', + agentId: '9fac4988-d88e-4d6c-994c-7495c14de8b9', entrypointTag: 'create_article' // local: true, host: '127.0.0.1', port: 8451, ), ); - print('πŸ—žοΈ Testing AI Journalist Agent (Streaming)'); + print('πŸ—žοΈ Testing AI Journalist Agent'); print('=' * 50); // Run article creation once (non-streaming) diff --git a/runagent/client/client.py b/runagent/client/client.py index ef84e4f..45ef9bb 100644 --- a/runagent/client/client.py +++ b/runagent/client/client.py @@ -22,7 +22,7 @@ def __init__(self, code: str, message: str, suggestion: str | None = None, detai class RunAgentClient: - def __init__(self, agent_id: str, entrypoint_tag: str, local: bool = True, host: str = None, port: int = None): + def __init__(self, agent_id: str, entrypoint_tag: str, local: bool = True, host: str = None, port: int = None, user_id: str = None, persistent_memory: bool = False): self.sdk = RunAgentSDK() self.serializer = CoreSerializer() self.local = local @@ -32,6 +32,10 @@ def __init__(self, agent_id: str, entrypoint_tag: str, local: bool = True, host: self.agent_host = None self.agent_port = None + # Persistent storage settings (set at client level) + self.user_id = user_id + self.persistent_memory = persistent_memory + # FIXED: Detect if this is a streaming entrypoint self.is_streaming = entrypoint_tag.endswith("_stream") @@ -68,6 +72,13 @@ def __init__(self, agent_id: str, entrypoint_tag: str, local: bool = True, host: def run(self, *input_args, **input_kwargs): """ FIXED: Smart execution - automatically uses streaming or non-streaming based on entrypoint + + Args: + *input_args: Positional arguments to pass to the agent + **input_kwargs: Keyword arguments to pass to the agent + + Note: user_id and persistent_memory are set at client initialization, not here. + The 'user' parameter in input_kwargs is for agent's internal session management. """ # FIXED: If this is a streaming entrypoint, automatically use run_stream if self.is_streaming: @@ -75,8 +86,11 @@ def run(self, *input_args, **input_kwargs): return self.run_stream(*input_args, **input_kwargs) # Non-streaming execution (HTTP POST) + # Use persistent storage settings from client initialization response = self.rest_client.run_agent( - self.agent_id, self.entrypoint_tag, input_args=input_args, input_kwargs=input_kwargs + self.agent_id, self.entrypoint_tag, + input_args=input_args, input_kwargs=input_kwargs, + user_id=self.user_id, persistent_memory=self.persistent_memory ) # Only print debug response in DISABLE_TRY_CATCH mode if os.getenv('DISABLE_TRY_CATCH'): @@ -126,10 +140,20 @@ def run(self, *input_args, **input_kwargs): raise self._build_error_from_string(response.get("error")) def run_stream(self, *input_args, **input_kwargs): - """Stream agent execution results in real-time via WebSocket""" + """Stream agent execution results in real-time via WebSocket + + Args: + *input_args: Positional arguments to pass to the agent + **input_kwargs: Keyword arguments to pass to the agent + + Note: user_id and persistent_memory are set at client initialization, not here. + The 'user' parameter in input_kwargs is for agent's internal session management. + """ socket_iterator = self.socket_client.run_stream( - self.agent_id, self.entrypoint_tag, input_args=input_args, input_kwargs=input_kwargs + self.agent_id, self.entrypoint_tag, + input_args=input_args, input_kwargs=input_kwargs, + user_id=self.user_id, persistent_memory=self.persistent_memory ) def _iterator(): diff --git a/runagent/sdk/deployment/remote.py b/runagent/sdk/deployment/remote.py index ac8107e..b59e4f1 100644 --- a/runagent/sdk/deployment/remote.py +++ b/runagent/sdk/deployment/remote.py @@ -98,10 +98,39 @@ def get_agent_info(self, agent_id: str) -> t.Dict[str, t.Any]: def run_agent( self, agent_id: str, input_data: t.Dict[str, t.Any] ) -> t.Dict[str, t.Any]: - """Run a remote agent""" - # This would need to be implemented in RestClient - # For now, return a placeholder - return {"success": False, "error": "Remote agent execution not yet implemented"} + """Run a remote agent + + Args: + agent_id: Agent identifier + input_data: Input data including messages, user_id, persistent_memory, etc. + + Returns: + Execution result + """ + # Extract persistent storage parameters + user_id = input_data.pop("user_id", None) + persistent_memory = input_data.pop("persistent_memory", False) + + # Extract entrypoint_tag from input_data or use default + entrypoint_tag = input_data.pop("entrypoint_tag", "default") + + # Extract input_args and input_kwargs + input_args = input_data.pop("input_args", []) + input_kwargs = input_data.pop("input_kwargs", {}) + + # Any remaining data goes to input_kwargs + if input_data: + input_kwargs.update(input_data) + + # Call RestClient's run_agent method + return self.client.run_agent( + agent_id=agent_id, + entrypoint_tag=entrypoint_tag, + input_args=input_args, + input_kwargs=input_kwargs, + user_id=user_id, + persistent_memory=persistent_memory, + ) def delete_agent(self, agent_id: str) -> t.Dict[str, t.Any]: """Delete a remote agent""" diff --git a/runagent/sdk/rest_client.py b/runagent/sdk/rest_client.py index 849d020..3a47567 100644 --- a/runagent/sdk/rest_client.py +++ b/runagent/sdk/rest_client.py @@ -1478,8 +1478,21 @@ def run_agent( input_kwargs: dict = None, timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS, async_execution: bool = False, + user_id: str = None, + persistent_memory: bool = False, ) -> Dict: - """Execute an agent with given parameters""" + """Execute an agent with given parameters + + Args: + agent_id: Agent identifier + entrypoint_tag: Entrypoint tag to execute + input_args: Input arguments + input_kwargs: Input keyword arguments + timeout_seconds: Timeout in seconds + async_execution: Whether to execute asynchronously + user_id: User ID for persistent storage + persistent_memory: Enable persistent storage + """ try: console.print(f"πŸ€– Executing agent: [bold magenta]{agent_id}[/bold magenta]") @@ -1491,6 +1504,12 @@ def run_agent( "timeout_seconds": timeout_seconds, "async_execution": async_execution } + + # Add persistent storage parameters if provided + if user_id is not None: + request_data["user_id"] = user_id + if persistent_memory: + request_data["persistent_memory"] = persistent_memory # Execute the agent try: diff --git a/runagent/sdk/sdk.py b/runagent/sdk/sdk.py index 024196d..2f0a552 100644 --- a/runagent/sdk/sdk.py +++ b/runagent/sdk/sdk.py @@ -313,6 +313,8 @@ def run_agent( messages: t.Optional[t.List[t.Dict[str, str]]] = None, config: t.Optional[t.Dict[str, t.Any]] = None, local: bool = True, + user_id: t.Optional[str] = None, + persistent_memory: bool = False, ) -> t.Dict[str, t.Any]: """ Run an agent with a message or conversation. @@ -323,6 +325,8 @@ def run_agent( messages: List of message objects with 'role' and 'content' config: Optional configuration local: Whether to run locally or remotely + user_id: User ID for persistent storage (required if persistent_memory=True) + persistent_memory: Enable persistent storage for this execution Returns: Agent execution result with proper error handling @@ -338,6 +342,14 @@ def run_agent( if config: input_data["config"] = config + # Add persistent storage parameters to input_data if provided + if user_id is not None: + input_data["user_id"] = user_id + if persistent_memory: + input_data["persistent_memory"] = persistent_memory + if user_id is None: + raise ValidationError("user_id is required when persistent_memory=True") + # Run the agent if local: result = self.local.run_agent(agent_id, input_data) diff --git a/runagent/sdk/socket_client.py b/runagent/sdk/socket_client.py index 4c33247..eb8f48d 100644 --- a/runagent/sdk/socket_client.py +++ b/runagent/sdk/socket_client.py @@ -58,7 +58,7 @@ def __init__( self._debug(f" - base_socket_url: {self.base_socket_url}") self._debug(f" - api_key: {'SET' if self.api_key else 'NOT SET'}") - async def run_stream_async(self, agent_id: str, entrypoint_tag: str, *input_args, **input_kwargs) -> AsyncIterator[Any]: + async def run_stream_async(self, agent_id: str, entrypoint_tag: str, *input_args, user_id=None, persistent_memory=False, **input_kwargs) -> AsyncIterator[Any]: """Stream agent execution results (async version)""" # FIXED: Build proper cloud URL with query param auth @@ -92,6 +92,11 @@ async def run_stream_async(self, agent_id: str, entrypoint_tag: str, *input_args "timeout_seconds": 600, "async_execution": False } + # Add persistent storage parameters if provided + if user_id is not None: + request_data["user_id"] = user_id + if persistent_memory: + request_data["persistent_memory"] = persistent_memory self._debug(f"Sending request: {request_data}") @@ -151,7 +156,7 @@ async def run_stream_async(self, agent_id: str, entrypoint_tag: str, *input_args cleaned_msg = self._clean_error_message(error_msg) raise Exception(cleaned_msg) - def run_stream(self, agent_id: str, entrypoint_tag: str, input_args, input_kwargs) -> Iterator[Any]: + def run_stream(self, agent_id: str, entrypoint_tag: str, input_args, input_kwargs, user_id=None, persistent_memory=False) -> Iterator[Any]: """Stream agent execution results (sync version)""" from websockets.sync.client import connect @@ -189,6 +194,11 @@ def run_stream(self, agent_id: str, entrypoint_tag: str, input_args, input_kwarg "timeout_seconds": 600, "async_execution": False } + # Add persistent storage parameters if provided + if user_id is not None: + request_data["user_id"] = user_id + if persistent_memory: + request_data["persistent_memory"] = persistent_memory self._debug(f"Sending request: {request_data}") diff --git a/runagent/utils/schema.py b/runagent/utils/schema.py index 6776e5a..69710ab 100644 --- a/runagent/utils/schema.py +++ b/runagent/utils/schema.py @@ -93,6 +93,13 @@ class RunAgentConfig(BaseModel): default_factory=lambda: {"type": "none"}, description="Authentication configuration" ) + + # Persistent storage configuration + persistent_folders: t.Optional[t.List[str]] = Field( + default_factory=list, + description="List of folder paths that should be persisted to /persistent mount point. " + "These folders will be automatically synced to persistent storage when persistent_memory is enabled." + ) def to_dict(self) -> dict: """Convert to dictionary with custom serialization""" diff --git a/test_scripts/python/client_test_agno_persistent.py b/test_scripts/python/client_test_agno_persistent.py new file mode 100644 index 0000000..c913bb4 --- /dev/null +++ b/test_scripts/python/client_test_agno_persistent.py @@ -0,0 +1,28 @@ +from runagent import RunAgentClient + +# Test 1: Non-streaming with session management +print("=" * 50) +print("Test 1: Non-streaming with session") +print("=" * 50) + +# Create client with persistent storage settings +# user_id and persistent_memory are set at client level for persistent storage +ra = RunAgentClient( + agent_id="c998c025-4c9f-4466-886e-94345efe654b", + entrypoint_tag="agno_print_response", + local=False, + user_id="khalid", # User ID for persistent storage (VM-level) + persistent_memory=True # Enable persistent storage +) + +# First message - creating a new session +# Note: 'user' parameter here is for agent's internal session management (different from user_id) +result = ra.run( + prompt="why you are so funny?", + user="khalid", # Agent's internal user ID for session management + # session_id="01136b0e-6a8f-45f8-8314-fb9a8308872d", + new_session=False) + +print(f"Session ID: {result['session_id']}") +print(f"User ID: {result['user_id']}") +print(f"Response: {result['content']}\n") \ No newline at end of file From 44129069d2af1c6eb50a7cfc3692682b1b3582b8 Mon Sep 17 00:00:00 2001 From: RadeenXALNW Date: Sat, 29 Nov 2025 19:15:12 +0000 Subject: [PATCH 2/8] fixed bugs and test persistence --- examples/agno_persistent/runagent.config.json | 2 +- .../lead-score-flow/runagent.config.json | 2 +- .../runagent_sdk/python/test_sdk.py | 2 +- examples/trip_planner/agent/requirements.txt | 5 +- .../trip_planner/agent/runagent.config.json | 58 +++--- examples/trip_planner/sdk/python/test.py | 4 +- templates/agno/agno_persistent/agent.py | 166 ++++++++++++++++++ .../agno/agno_persistent/requirements.txt | 2 + .../agno/agno_persistent/runagent.config.json | 33 ++++ templates/agno/default/runagent.config.json | 2 +- test_scripts/python/client_test_agno.py | 4 +- .../python/client_test_agno_persistent.py | 42 ++++- 12 files changed, 281 insertions(+), 41 deletions(-) create mode 100644 templates/agno/agno_persistent/agent.py create mode 100644 templates/agno/agno_persistent/requirements.txt create mode 100644 templates/agno/agno_persistent/runagent.config.json diff --git a/examples/agno_persistent/runagent.config.json b/examples/agno_persistent/runagent.config.json index 80fe7c5..5bd5a98 100644 --- a/examples/agno_persistent/runagent.config.json +++ b/examples/agno_persistent/runagent.config.json @@ -25,7 +25,7 @@ ] }, "env_vars": {}, - "agent_id": "c998c025-4c9f-4466-886e-94345efe654b", + "agent_id": "b298c025-4c9f-4466-886e-14745efe664b", "auth_settings": { "type": "api_key" }, diff --git a/examples/lead-agent/lead-score-flow/runagent.config.json b/examples/lead-agent/lead-score-flow/runagent.config.json index 91c4417..7e6609c 100644 --- a/examples/lead-agent/lead-score-flow/runagent.config.json +++ b/examples/lead-agent/lead-score-flow/runagent.config.json @@ -25,7 +25,7 @@ ] }, "env_vars": {}, - "agent_id": "be87871d-b4c4-4ec1-990b-bef0ea4766f7", + "agent_id": "be89371d-b4c4-4ec1-990b-bef0ea4766f7", "auth_settings": { "type": "api_key" } diff --git a/examples/lead-agent/runagent_sdk/python/test_sdk.py b/examples/lead-agent/runagent_sdk/python/test_sdk.py index 4d82436..617088f 100644 --- a/examples/lead-agent/runagent_sdk/python/test_sdk.py +++ b/examples/lead-agent/runagent_sdk/python/test_sdk.py @@ -2,7 +2,7 @@ from runagent import RunAgentClient client = RunAgentClient( - agent_id="89aef000-0000-0000-0000-000000000000", + agent_id="be89371d-b4c4-4ec1-990b-bef0ea4766f7", entrypoint_tag="lead_score_flow", local=False ) diff --git a/examples/trip_planner/agent/requirements.txt b/examples/trip_planner/agent/requirements.txt index ea5b57c..e58f4e5 100644 --- a/examples/trip_planner/agent/requirements.txt +++ b/examples/trip_planner/agent/requirements.txt @@ -1,4 +1,5 @@ -ag2 +autogen==0.10.1 pydantic requests -openai \ No newline at end of file +openai +ag2[openai] \ No newline at end of file diff --git a/examples/trip_planner/agent/runagent.config.json b/examples/trip_planner/agent/runagent.config.json index 54bc53e..34012c5 100644 --- a/examples/trip_planner/agent/runagent.config.json +++ b/examples/trip_planner/agent/runagent.config.json @@ -1,27 +1,33 @@ { - "agent_name": "Trip Planner Agent", - "description": "AI-powered trip planning assistant with GraphRAG", - "framework": "ag2", - "template": "custom", - "version": "1.0.0", - "created_at": "2025-10-29 00:00:00", - "template_source": { - "repo_url": "https://github.com/runagent-dev/runagent.git", - "path": "templates/ag2/custom", - "author": "custom" - }, - "agent_architecture": { - "entrypoints": [ - { - "file": "trip_agent.py", - "module": "create_trip_planner", - "tag": "trip_create" - }, - { - "file": "trip_agent.py", - "module": "create_trip_planner_stream", - "tag": "trip_stream" - } - ] - } - } \ No newline at end of file + "agent_name": "trip planner", + "description": "My AI agent", + "framework": "default", + "template": "", + "version": "1.0.0", + "created_at": "2025-11-29T16:54:25.565279", + "template_source": { + "repo_url": "https://github.com/runagent-dev/runagent.git", + "author": "runagent-cli", + "path": "/home/azureuser/runagent/examples/trip_planner/agent" + }, + "agent_architecture": { + "entrypoints": [ + { + "file": "trip_agent.py", + "module": "create_trip_planner", + "tag": "trip_create" + }, + { + "file": "trip_agent.py", + "module": "create_trip_planner_stream", + "tag": "trip_stream" + } + ] + }, + "env_vars": {}, + "agent_id": "7ca76f5d-9524-4614-b283-d9a50db147b8", + "auth_settings": { + "type": "api_key" + }, + "persistent_folders": [] +} \ No newline at end of file diff --git a/examples/trip_planner/sdk/python/test.py b/examples/trip_planner/sdk/python/test.py index ad26871..48a2bf9 100644 --- a/examples/trip_planner/sdk/python/test.py +++ b/examples/trip_planner/sdk/python/test.py @@ -7,7 +7,7 @@ from runagent import RunAgentClient # UPDATE THIS! -AGENT_ID = "be1eef6e-2700-4980-b808-e94b3394e747" +AGENT_ID = "7ca76f5d-9524-4614-b283-d9a50db147b8" # ============================================ @@ -43,7 +43,7 @@ # stream_client = RunAgentClient( # agent_id=AGENT_ID, # entrypoint_tag="trip_stream", -# local=True +# local=False # ) # print("\nπŸ“‘ Streaming response:\n") diff --git a/templates/agno/agno_persistent/agent.py b/templates/agno/agno_persistent/agent.py new file mode 100644 index 0000000..3cd5a08 --- /dev/null +++ b/templates/agno/agno_persistent/agent.py @@ -0,0 +1,166 @@ +from textwrap import dedent +from typing import List, Optional +from agno.agent import Agent +from agno.db.base import SessionType +from agno.db.sqlite import SqliteDb +from agno.models.openai import OpenAIChat +from agno.session import AgentSession + +# Single SQLite DB for all agents/sessions +# Note: The backend automatically maps "rad" folder to /persistent/rad +# So we can use relative path "rad/agents.db" and it will persist across VM restarts +db = SqliteDb(db_file="simi/agents.db") + + +def get_chat_agent( + user: str = "user", + new_session: bool = True, + session_id: Optional[str] = None, +) -> Agent: + """ + Create and return a simple persistent chatbot Agent. + + Args: + user: User ID to associate with the agent. + new_session: If False, will try to resume the latest existing session + for this user (unless session_id is provided). + session_id: If provided and new_session=False, use this specific session. + + Returns: + An initialized Agent instance. + """ + resolved_session_id: Optional[str] = None + + if not new_session: + # If caller gave a specific session_id, use it + if session_id is not None: + resolved_session_id = session_id + else: + # Otherwise, try to resume the latest existing session for this user + existing_sessions: List[AgentSession] = db.get_sessions( # type: ignore + user_id=user, + session_type=SessionType.AGENT, + ) + if len(existing_sessions) > 0: + resolved_session_id = existing_sessions[0].session_id + + agent = Agent( + user_id=user, + session_id=resolved_session_id, + model=OpenAIChat(id="gpt-4o-mini"), + instructions=dedent( + """\ + You are a helpful, general-purpose AI assistant. + + Goals: + - Answer questions clearly and concisely. + - Ask for clarification when needed. + - Use a friendly, professional tone. + + You have access to the conversation history via the session. + Use that context to keep multi-turn conversations coherent. + """ + ), + db=db, + # Persist and reuse chat history across runs for this session + read_chat_history=True, + markdown=True, + ) + + return agent + + +def agent_print_response(prompt: str = None, user: str = "user", session_id: Optional[str] = None, new_session: bool = False, **kwargs): + """ + Non-streaming response with session management. + + Args: + prompt: User's message/prompt (can also be passed as 'message' in kwargs) + user: User ID for session management (default: "user") + session_id: Optional specific session ID to resume (default: None, will auto-resume latest) + new_session: If True, creates a new session. If False, resumes existing session (default: False) + **kwargs: Additional parameters (supports 'message' as alias for 'prompt') + + Returns: + Serializable response content with session metadata + """ + # Handle prompt from kwargs if not provided directly + if prompt is None: + prompt = kwargs.get('message', '') + + agent = get_chat_agent(user=user, new_session=new_session, session_id=session_id) + + # Get the response object + response = agent.run(prompt) + + # Extract the actual content from the response object + if hasattr(response, 'content'): + return { + "content": response.content, + "session_id": agent.session_id, + "user_id": agent.user_id + } + elif hasattr(response, 'messages') and response.messages: + # Get the last message content + content = response.messages[-1].content if hasattr(response.messages[-1], 'content') else str(response.messages[-1]) + return { + "content": content, + "session_id": agent.session_id, + "user_id": agent.user_id + } + elif hasattr(response, 'text'): + return { + "content": response.text, + "session_id": agent.session_id, + "user_id": agent.user_id + } + else: + # Fallback: convert to string + return { + "content": str(response), + "session_id": agent.session_id, + "user_id": agent.user_id + } + + +def agent_print_response_stream(prompt: str = None, user: str = "user", session_id: Optional[str] = None, new_session: bool = False, **kwargs): + """ + Streaming response with session management. + + Args: + prompt: User's message/prompt (can also be passed as 'message' in kwargs) + user: User ID for session management (default: "user") + session_id: Optional specific session ID to resume (default: None, will auto-resume latest) + new_session: If True, creates a new session. If False, resumes existing session (default: False) + **kwargs: Additional parameters (supports 'message' as alias for 'prompt') + + Yields: + Serializable chunks with content and session metadata + """ + # Handle prompt from kwargs if not provided directly + if prompt is None: + prompt = kwargs.get('message', '') + + agent = get_chat_agent(user=user, new_session=new_session, session_id=session_id) + + # Yield session info first + yield { + "type": "session_info", + "session_id": agent.session_id, + "user_id": agent.user_id + } + + # Stream the response + for chunk in agent.run(prompt, stream=True): + yield { + "type": "content", + "content": chunk.content if hasattr(chunk, 'content') else str(chunk) + } + + # Yield final session confirmation + yield { + "type": "session_end", + "session_id": agent.session_id + } + + diff --git a/templates/agno/agno_persistent/requirements.txt b/templates/agno/agno_persistent/requirements.txt new file mode 100644 index 0000000..832f552 --- /dev/null +++ b/templates/agno/agno_persistent/requirements.txt @@ -0,0 +1,2 @@ +agno +openai \ No newline at end of file diff --git a/templates/agno/agno_persistent/runagent.config.json b/templates/agno/agno_persistent/runagent.config.json new file mode 100644 index 0000000..ba56dfc --- /dev/null +++ b/templates/agno/agno_persistent/runagent.config.json @@ -0,0 +1,33 @@ +{ + "agent_name": "Agno Memory", + "description": "My AI agent", + "framework": "default", + "template": "", + "version": "1.0.0", + "created_at": "2025-11-28T20:59:22.479436", + "template_source": { + "repo_url": "https://github.com/runagent-dev/runagent.git", + "author": "runagent-cli", + "path": "/home/azureuser/runagent/templates/agno/agno_persistent" + }, + "agent_architecture": { + "entrypoints": [ + { + "file": "agent.py", + "module": "agent_print_response", + "tag":"agno_print_response" + }, + { + "file": "agent.py", + "module": "agent_print_response_stream", + "tag":"agno_print_response_stream" + } + ] + }, + "env_vars": {}, + "agent_id": "c778c025-4c9f-4466-886e-14845efe664b", + "auth_settings": { + "type": "api_key" + }, + "persistent_folders": ["simi"] +} \ No newline at end of file diff --git a/templates/agno/default/runagent.config.json b/templates/agno/default/runagent.config.json index fe99734..ff2f17d 100644 --- a/templates/agno/default/runagent.config.json +++ b/templates/agno/default/runagent.config.json @@ -25,7 +25,7 @@ ] }, "env_vars": {}, - "agent_id": "ae29bd73-b3d3-42c8-a98f-5d7aec7ee919", + "agent_id": "ae29bd73-b3d3-22c8-a98f-5d7aec7ee919", "auth_settings": { "type": "api_key" } diff --git a/test_scripts/python/client_test_agno.py b/test_scripts/python/client_test_agno.py index a376da5..c77ff38 100644 --- a/test_scripts/python/client_test_agno.py +++ b/test_scripts/python/client_test_agno.py @@ -1,7 +1,7 @@ # from runagent import RunAgentClient # ra = RunAgentClient( -# agent_id="55ddf000-0000-0000-0000-000000000000", +# agent_id="ae29bd73-b3d3-22c8-a98f-5d7aec7ee919", # entrypoint_tag="agno_print_response", # local=False # ) @@ -17,7 +17,7 @@ from runagent import RunAgentClient ra = RunAgentClient( - agent_id="ad29bd73-b3d3-42c8-a98f-5d7aec7ee919", + agent_id="ae29bd73-b3d3-22c8-a98f-5d7aec7ee919", entrypoint_tag="agno_print_response_stream", local=False ) diff --git a/test_scripts/python/client_test_agno_persistent.py b/test_scripts/python/client_test_agno_persistent.py index c913bb4..6c46c8f 100644 --- a/test_scripts/python/client_test_agno_persistent.py +++ b/test_scripts/python/client_test_agno_persistent.py @@ -1,3 +1,34 @@ +# from runagent import RunAgentClient + +# # Test 1: Non-streaming with session management +# print("=" * 50) +# print("Test 1: Non-streaming with session") +# print("=" * 50) + +# # Create client with persistent storage settings +# # user_id and persistent_memory are set at client level for persistent storage +# ra = RunAgentClient( +# agent_id="b298c025-4c9f-4466-886e-14745efe664b", +# entrypoint_tag="agno_print_response", +# local=False, +# user_id="khalid", # User ID for persistent storage (VM-level) +# persistent_memory=True # Enable persistent storage +# ) + +# # First message - creating a new session +# # Note: 'user' parameter here is for agent's internal session management (different from user_id) +# result = ra.run( +# prompt="i told you my dream job. please tell me what was it?", +# user="khalid", # Agent's internal user ID for session management +# session_id="fac40913-fb9c-41aa-821d-2bc204f9daf2", +# new_session=False) + +# print(f"Session ID: {result['session_id']}") +# print(f"User ID: {result['user_id']}") +# print(f"Response: {result['content']}\n") + + + from runagent import RunAgentClient # Test 1: Non-streaming with session management @@ -8,19 +39,20 @@ # Create client with persistent storage settings # user_id and persistent_memory are set at client level for persistent storage ra = RunAgentClient( - agent_id="c998c025-4c9f-4466-886e-94345efe654b", + agent_id="c778c025-4c9f-4466-886e-14845efe664b", entrypoint_tag="agno_print_response", local=False, - user_id="khalid", # User ID for persistent storage (VM-level) + user_id="prova", # User ID for persistent storage (VM-level) persistent_memory=True # Enable persistent storage ) # First message - creating a new session # Note: 'user' parameter here is for agent's internal session management (different from user_id) result = ra.run( - prompt="why you are so funny?", - user="khalid", # Agent's internal user ID for session management - # session_id="01136b0e-6a8f-45f8-8314-fb9a8308872d", + prompt="can you tell me my favorite movie and tv series? I told you before. I told you both.", + user="prova", # Agent's internal user ID for session management + # session_id="ccb19879-5416-4715-96d3-f62036ccf429", + session_id="f927c786-87c1-45c4-a201-aaa1711b8f53", new_session=False) print(f"Session ID: {result['session_id']}") From 131a5382e98f5dfb113057365f4e7a1cae45ef3c Mon Sep 17 00:00:00 2001 From: RadeenXALNW Date: Sat, 29 Nov 2025 23:49:58 +0000 Subject: [PATCH 3/8] fixed serialization fallback issue for persistence --- examples/lightrag_persistent/README.md | 148 ++++++++++++++++++ examples/lightrag_persistent/agent.py | 81 ++++++++++ examples/lightrag_persistent/requirements.txt | 3 + .../lightrag_persistent/runagent.config.json | 33 ++++ runagent/client/client.py | 21 ++- runagent/utils/serializer.py | 32 ++++ templates/agno/default/runagent.config.json | 2 +- test/rag_test.txt | 123 +++++++++++++++ test/test_lightrag.py | 96 ++++++++++++ test_scripts/python/client_test_agno.py | 2 +- test_scripts/python/client_test_lightrag.py | 61 ++++++++ 11 files changed, 598 insertions(+), 4 deletions(-) create mode 100644 examples/lightrag_persistent/README.md create mode 100644 examples/lightrag_persistent/agent.py create mode 100644 examples/lightrag_persistent/requirements.txt create mode 100644 examples/lightrag_persistent/runagent.config.json create mode 100644 test/rag_test.txt create mode 100644 test/test_lightrag.py create mode 100644 test_scripts/python/client_test_lightrag.py diff --git a/examples/lightrag_persistent/README.md b/examples/lightrag_persistent/README.md new file mode 100644 index 0000000..7c39afa --- /dev/null +++ b/examples/lightrag_persistent/README.md @@ -0,0 +1,148 @@ +# LightRAG RunAgent Integration + +A RunAgent-compatible implementation of LightRAG for document ingestion and intelligent querying. + +## Features + +- **Document Ingestion**: Ingest text documents into a persistent RAG system +- **Intelligent Querying**: Query documents using multiple search modes (naive, local, global, hybrid) +- **Persistent Storage**: Data persists across VM restarts using RunAgent's persistent folders +- **Dual Entrypoints**: Separate functions for ingesting and querying + +## File Structure + +``` +lightrag_agent/ +β”œβ”€β”€ agent.py # Main agent implementation +β”œβ”€β”€ requirements.txt # Python dependencies +β”œβ”€β”€ runagent.config.json # RunAgent configuration +β”œβ”€β”€ test_sdk.py # SDK test script +└── README.md # This file +``` + +## Setup + +1. Deploy the agent using RunAgent CLI: +```bash +runagent deploy +``` + +2. Note the `agent_id` from the deployment output + +3. Update `test_sdk.py` with your `agent_id`: +```python +AGENT_ID = "your-agent-id-here" +``` + +## Usage + +### Ingest Text from File + +```python +from runagent import RunAgentClient + +ingest_client = RunAgentClient( + agent_id="your-agent-id", + entrypoint_tag="ingest_text", + local=False, + user_id="your_user_id", + persistent_memory=True +) + +# Ingest from file +with open("document.txt", 'r') as f: + text = f.read() + +result = ingest_client.run(text=text) +print(result) +``` + +### Query the RAG System + +```python +from runagent import RunAgentClient + +query_client = RunAgentClient( + agent_id="your-agent-id", + entrypoint_tag="query_rag", + local=False, + user_id="your_user_id", + persistent_memory=True +) + +# Query with hybrid mode (default) +result = query_client.run( + query="What are the main themes?", + mode="hybrid" # Options: "naive", "local", "global", "hybrid" +) + +print(result['result']) +``` + +## Query Modes + +- **naive**: Simple text matching +- **local**: Context-aware local search +- **global**: Broad semantic search +- **hybrid**: Combines local and global for best results (recommended) + +## Testing + +Run the included test script: + +```bash +python test_sdk.py +``` + +This will: +1. Ingest sample text about LightRAG +2. Run multiple queries with different modes +3. Display results for each query + +## Environment Variables + +Required environment variables (set in your RunAgent environment): +- `OPENAI_API_KEY`: Your OpenAI API key + +## Persistent Storage + +The RAG database is stored in `rad/rag_storage/` which persists across VM restarts thanks to RunAgent's persistent folders configuration. + +## API Reference + +### ingest_text(text, **kwargs) + +Ingest text into the RAG system. + +**Parameters:** +- `text` (str): Text content to ingest +- `**kwargs`: Additional parameters + +**Returns:** +```python +{ + "status": "success", + "message": "Successfully ingested X characters", + "working_dir": "rad/rag_storage" +} +``` + +### query_rag(query, mode="hybrid", **kwargs) + +Query the RAG system. + +**Parameters:** +- `query` (str): Question or search query +- `mode` (str): Search mode ("naive", "local", "global", "hybrid") +- `**kwargs`: Additional parameters + +**Returns:** +```python +{ + "status": "success", + "query": "Your question", + "mode": "hybrid", + "result": "Answer from RAG system", + "working_dir": "rag_storage" +} +``` \ No newline at end of file diff --git a/examples/lightrag_persistent/agent.py b/examples/lightrag_persistent/agent.py new file mode 100644 index 0000000..378f8e2 --- /dev/null +++ b/examples/lightrag_persistent/agent.py @@ -0,0 +1,81 @@ +import os +import asyncio +from dotenv import load_dotenv +from lightrag import LightRAG, QueryParam +from lightrag.llm.openai import gpt_4o_mini_complete, openai_embed +from lightrag.kg.shared_storage import initialize_pipeline_status + +load_dotenv() + +WORKING_DIR = "rag_storage" + + +async def get_rag_instance(): + if not os.path.exists(WORKING_DIR): + os.makedirs(WORKING_DIR, exist_ok=True) + + rag = LightRAG( + working_dir=WORKING_DIR, + embedding_func=openai_embed, + llm_model_func=gpt_4o_mini_complete, + ) + + await rag.initialize_storages() + await initialize_pipeline_status() + + return rag + + +async def ingest_text_async(text: str = None, **kwargs): + if text is None: + text = kwargs.get('content', '') + + if not text: + return {"status": "error", "message": "No text provided"} + + rag = None + try: + rag = await get_rag_instance() + await rag.ainsert(text) + return {"status": "success", "message": f"Ingested {len(text)} characters"} + except Exception as e: + return {"status": "error", "message": str(e)} + finally: + if rag: + try: + await rag.finalize_storages() + except: + pass + + +async def query_rag_async(query: str = None, mode: str = "hybrid", **kwargs): + if query is None: + query = kwargs.get('question', '') + + if not query: + return {"status": "error", "message": "No query provided"} + + if mode not in ["naive", "local", "global", "hybrid"]: + mode = "hybrid" + + rag = None + try: + rag = await get_rag_instance() + result = await rag.aquery(query, param=QueryParam(mode=mode)) + return {"status": "success", "query": query, "mode": mode, "result": result} + except Exception as e: + return {"status": "error", "message": str(e)} + finally: + if rag: + try: + await rag.finalize_storages() + except: + pass + + +def ingest_text(text: str = None, **kwargs): + return asyncio.run(ingest_text_async(text, **kwargs)) + + +def query_rag(query: str = None, mode: str = "hybrid", **kwargs): + return asyncio.run(query_rag_async(query, mode, **kwargs)) \ No newline at end of file diff --git a/examples/lightrag_persistent/requirements.txt b/examples/lightrag_persistent/requirements.txt new file mode 100644 index 0000000..dbe88a4 --- /dev/null +++ b/examples/lightrag_persistent/requirements.txt @@ -0,0 +1,3 @@ +lightrag-hku +openai +python-dotenv \ No newline at end of file diff --git a/examples/lightrag_persistent/runagent.config.json b/examples/lightrag_persistent/runagent.config.json new file mode 100644 index 0000000..3fad432 --- /dev/null +++ b/examples/lightrag_persistent/runagent.config.json @@ -0,0 +1,33 @@ +{ + "agent_name": "LightRAG", + "description": "Lightrag is agentic RAG framework", + "framework": "default", + "template": "", + "version": "1.0.0", + "created_at": "2025-11-29T20:52:59.435244", + "template_source": { + "repo_url": "https://github.com/runagent-dev/runagent.git", + "author": "runagent-cli", + "path": "/home/azureuser/runagent/examples/lightrag_persistent" + }, + "agent_architecture": { + "entrypoints": [ + { + "file": "agent.py", + "module": "ingest_text", + "tag": "ingest_text" + }, + { + "file": "agent.py", + "module": "query_rag", + "tag": "query_rag" + } + ] + }, + "env_vars": {}, + "agent_id": "63751c14-0ed5-426c-ab44-aa94e3505bed", + "auth_settings": { + "type": "api_key" + }, + "persistent_folders": ["rag_storage"] +} \ No newline at end of file diff --git a/runagent/client/client.py b/runagent/client/client.py index 45ef9bb..998c495 100644 --- a/runagent/client/client.py +++ b/runagent/client/client.py @@ -109,15 +109,32 @@ def run(self, *input_args, **input_kwargs): # Backward compatibility for very old responses elif "output_data" in response: response_payload = response.get("output_data") + # Check if data_field itself is a dict (direct response) + elif isinstance(data_field, dict): + # If data_field is a dict, it might be the actual response + response_payload = data_field if response_payload is None: return None + # Check for empty string + if isinstance(response_payload, str) and not response_payload.strip(): + return None + if isinstance(response_payload, str): try: return self.serializer.deserialize_object_from_structured(response_payload) - except Exception: - pass + except Exception as e: + # Log the error for debugging but try fallback + import logging + logger = logging.getLogger(__name__) + logger.debug(f"Failed to deserialize structured data, trying fallback: {e}") + # Try direct deserialization as fallback + try: + return self.serializer.deserialize_object(response_payload) + except Exception: + # If both fail, return None or the raw string + return response_payload if response_payload else None return self.serializer.deserialize_object(response_payload) diff --git a/runagent/utils/serializer.py b/runagent/utils/serializer.py index e7995bb..526b7f5 100644 --- a/runagent/utils/serializer.py +++ b/runagent/utils/serializer.py @@ -58,6 +58,11 @@ def deserialize_object(self, json_str: str, reconstruct: bool = False) -> Union[ raise ValueError(f"Expected string input, got {type(json_str)}") try: + # Check if string is empty or whitespace only + if not json_str or not json_str.strip(): + self.logger.warning("Empty JSON string provided to deserialize_object") + return None + deserialized_data = json.loads(json_str) if not reconstruct: @@ -237,6 +242,11 @@ def deserialize_object_from_structured(self, structured_json: str) -> Any: if not isinstance(structured_json, str): raise ValueError(f"Expected JSON string input, got {type(structured_json)}") + # Check if string is empty or whitespace only + if not structured_json or not structured_json.strip(): + self.logger.warning("Empty structured JSON string provided") + raise ValueError("Empty structured JSON string") + structured_data = json.loads(structured_json) if not isinstance(structured_data, dict): @@ -248,26 +258,48 @@ def deserialize_object_from_structured(self, structured_json: str) -> Any: if data_type is None or payload is None: raise ValueError("Structured data must have 'type' and 'payload' keys") + # Check if payload is empty string + if isinstance(payload, str) and not payload.strip(): + if data_type == "null": + return None + elif data_type == "string": + return "" + else: + self.logger.warning(f"Empty payload for type '{data_type}', returning None") + return None + # Parse payload based on type if data_type == "null": return None elif data_type == "string": # Payload is JSON string, parse to get the actual string + if not payload or not payload.strip(): + return "" return json.loads(payload) elif data_type == "integer": + if not payload or not payload.strip(): + return 0 value = json.loads(payload) return int(value) elif data_type == "number": + if not payload or not payload.strip(): + return 0.0 value = json.loads(payload) return float(value) elif data_type == "boolean": + if not payload or not payload.strip(): + return False return json.loads(payload) elif data_type in ("array", "object"): # Parse JSON to get structured data + if not payload or not payload.strip(): + return [] if data_type == "array" else {} return json.loads(payload) else: # Unknown type, try to parse as JSON self.logger.warning(f"Unknown type '{data_type}', attempting JSON parse") + if not payload or not payload.strip(): + return None return json.loads(payload) except json.JSONDecodeError as e: diff --git a/templates/agno/default/runagent.config.json b/templates/agno/default/runagent.config.json index ff2f17d..8fd811e 100644 --- a/templates/agno/default/runagent.config.json +++ b/templates/agno/default/runagent.config.json @@ -25,7 +25,7 @@ ] }, "env_vars": {}, - "agent_id": "ae29bd73-b3d3-22c8-a98f-5d7aec7ee919", + "agent_id": "ae29bd73-b3d3-99c8-a98f-5d7aec7ee919", "auth_settings": { "type": "api_key" } diff --git a/test/rag_test.txt b/test/rag_test.txt new file mode 100644 index 0000000..3506599 --- /dev/null +++ b/test/rag_test.txt @@ -0,0 +1,123 @@ +Here is a long β€œworld-overview” text β€” summarizing some of the most important recent global developments, with numbers, trends, and context (as of 2025). You can save this as a text file if you like. + +--- + +# The World in 2025: Key Numbers and Big Trends 🌍 + +## Global Population & Demographics + +* As of late 2025, the world population is estimated at about **8.25 billion people**. ([DataReportal – Global Digital Insights][1]) + +* The global population continues to grow, but the annual growth rate has slowed compared to previous decades β€” roughly **0.8–0.9% per year** in 2024–2025. ([Jagranjosh.com][2]) + +* According to projections by global institutions, the world population is expected to peak at around **10.3 billion** by the 2080s, then gradually decline to about **10.2 billion** by the end of the century. ([un.org][3]) + +* Though global population continues rising, demographic shifts are uneven: many countries already see declining fertility rates or aging populations. ([OECD][4]) + +* Urbanization continues strongly: more people live in cities than ever before. The share of people living in urban areas globally has nearly doubled since 1950. ([The Guardian][5]) + +* The world now has many more β€œmegacities” (cities with 10 million+ population) than several decades ago: from 8 in 1975 to 33 in 2025. ([The Guardian][5]) + +* According to a recent 2025 report by the United Nations (UN), the biggest-city ranking globally shifted: Jakarta (Indonesia) has now overtaken Tokyo (Japan) as the world’s most populous city, with an estimated **42 million residents**, followed by Dhaka (Bangladesh) with **37 million**, and Tokyo with **33 million**. ([The Guardian][5]) + +**Implication:** +Humanity continues to grow, but population growth is slowing and will likely peak mid-century. Meanwhile, urbanization accelerates. The global demographic landscape is shifting β€” more people live in cities, more megacities emerge, and urban density increases. + +--- + +## Economy & Global Growth + +* The global economy in 2025 is projected to grow by around **3.0%** according to the International Monetary Fund (IMF). ([IMF][6]) + +* However, long-term structural concerns remain. According to the Organisation for Economic Co-operation and Development (OECD), global potential growth is expected to moderate over coming decades β€” in part because labour-force growth (especially working-age population growth) is slowing. ([OECD][4]) + +* Broad global risks β€” including economic instability, geopolitical tensions, and environmental stressors β€” are highlighted in the 2025 edition of the Global Risks Report 2025. ([World Economic Forum][7]) + +**Implication:** +While the world economy continues to grow, momentum is weaker than in past decades. Economic growth may be constrained by demographic changes (aging populations, slower labour growth), and by broader structural risks β€” meaning sustainable development may require significant adaptation and reform. + +--- + +## Climate, Environment & Global Risk + +* According to the World Meteorological Organization (WMO), **2024 was the warmest year on record**β€”marking a continuing trend of global warming. ([World Meteorological Organization][8]) + +* The increase in greenhouse-gas concentrations (COβ‚‚, methane, nitrous oxide) in 2024 reached unprecedented levels, driven by human emissions, wildfires, and reduced carbon β€œsinks” (like forests and oceans). ([un.org][9]) + +* As a direct consequence, the world is experiencing more frequent and intense climate disasters. Across 2024, there were **more than 150 β€œunprecedented” extreme-weather events** (heatwaves, floods, storms) globally β€” the largest number ever recorded in a single year. ([The Guardian][10]) + +* The human toll is serious: the recent Lancet Countdown Report 2025 estimated that heat-related deaths have increased by 23% since the 1990s, and in 2024 the average person was exposed to ~16 days of dangerous heat (including infants and older adults who faced even more severe exposure). ([World Health Organization][11]) + +* The environmental challenges go beyond climate: global issues now include biodiversity loss, pollution, water scarcity, and resource stress β€” all exacerbated by population growth and increased consumption. ([un.org][12]) + +**Implication:** +The climate crisis is no longer a future threat β€” it is reshaping lives, economies, and stability today. Global warming, extreme weather, and environmental degradation pose serious, immediate risks to ecosystems, human health, food security, infrastructure, and livelihood β€” especially in vulnerable countries. + +--- + +## Societal & Developmental Challenges + +* Despite global population growth, many countries now face slowing fertility rates or even population decline β€” especially in developed or rapidly aging nations. ([un.org][3]) + +* Slower population growth plus aging demographics could suppress long-term economic growth potential, strain public services (especially with older populations), and change labor market dynamics globally. ([OECD][4]) + +* Meanwhile, urbanization β€” though offering opportunities β€” also presents challenges: mega-cities require massive infrastructure, social support, housing, sanitation, and services. Rapid urban growth can exacerbate inequality, congestion, resource demand, and pollution. + +* At the global governance level, risks highlighted by the Global Risks Report 2025 emphasize geopolitical tensions, social polarization, technological disruption, and environmental crises as interconnected challenges that societies must navigate carefully. ([World Economic Forum][7]) + +**Implication:** +The world is entering a complex era where demographic shifts, urbanization, economic constraints, and environmental pressures intersect. Managing these transitions will require coordinated global and local policies, investments in resilience, social support, sustainable development, and adaptive governance. + +--- + +## Looking Forward: What the Projections Suggest + +* The world population may peak mid-century (~2080s) at ~**10.3 billion**, then slightly decline to ~**10.2 billion** by 2100. ([un.org][3]) +* Economic growth may continue but at a slower rate, with structural headwinds like slowing labour growth, ageing populations, and increased risk exposure. ([OECD][4]) +* Environmental pressures will mount β€” climate change, resource scarcity, biodiversity loss, water stress, and pollution β€” demanding urgent global adaptation efforts. ([un.org][9]) +* Urbanization will deepen; megacities will increase and expand. This could offer economic opportunities, but only if infrastructural and social investments keep pace. ([The Guardian][5]) +* Inequality β€” between countries, and within cities β€” may widen if proactive social and economic policies are not adopted globally. + +--- + +## Why This Matters (Especially for People / Countries Like Bangladesh) + +Given that you're located in Dhaka Division, Bangladesh β€” a region likely to be deeply affected by many of these global trends β€” you should be aware: + +* Rapid urbanization means Dhaka and similar cities may grow further, increasing stress on infrastructure, housing, sanitation, health, and employment. +* Climate change is a global phenomenon with local impact: rising seas, floods, extreme weather, and heat waves may disproportionately affect low-lying and densely populated regions. Already, Bangladesh is considered highly vulnerable to climate risks. ([The Climate Reality Project][13]) +* Slowing global economic growth and structural headwinds may reduce economic opportunities, especially if global supply chains, trade, and labour dynamics shift. +* International cooperation and sustainable development β€” on environment, urban planning, social protection β€” will be increasingly important. What happens globally (policies, climate action, economic stability) will have ripple effects locally. + +--- + +# Concluding Thoughts + +We live in a time of deep global transition: population growth, urbanization, environmental upheaval, shifting economic dynamics, and demographic transformations are all converging. The next few decades β€” leading up to mid-century when global population peaks β€” are likely to shape the long-term future of humanity. + +For individuals, communities, and nations β€” especially those in vulnerable regions β€” awareness, adaptation, resilience, and sustainable planning will be critical. The challenges are formidable. But if societies invest in sustainable infrastructure, equitable governance, climate resilience, and human capital, it’s possible to navigate this transition responsibly. + +--- + +If you like, I can **prepare a full β€œ2025 Global Overview Report”** for you β€” with **10+ pages**, **data tables**, and **charts** (population by region, urbanization trends, climate data, economic forecasts). +Would you like me to build that now? + +* [The Guardian](https://www.theguardian.com/world/2025/nov/27/jakarta-overtakes-tokyo-most-populous-city-world?utm_source=chatgpt.com) +* [The Guardian](https://www.theguardian.com/environment/2025/mar/19/unprecedented-climate-disasters-extreme-weather-un-report?utm_source=chatgpt.com) +* [Reuters](https://www.reuters.com/business/finance/ageing-populations-ticking-time-bomb-gdp-growth-says-ebrd-2025-11-25/?utm_source=chatgpt.com) +* [Financial Times](https://www.ft.com/content/c2f7c366-ab76-47fd-bd23-6424b29067ca?utm_source=chatgpt.com) +* [time.com](https://time.com/7294161/fertility-rate-decline/?utm_source=chatgpt.com) + +[1]: https://datareportal.com/reports/digital-2026-global-population-trends?utm_source=chatgpt.com "Digital 2026: global population trends" +[2]: https://www.jagranjosh.com/general-knowledge/top-ten-most-populated-nations-of-the-world-1393925211-1?utm_source=chatgpt.com "Top 10 Most Populated Nations of the World (2025)" +[3]: https://www.un.org/en/global-issues/population?utm_source=chatgpt.com "Population - the United Nations" +[4]: https://www.oecd.org/en/topics/sub-issues/economic-outlook/long-run-economic-scenarios-2025-update.html?utm_source=chatgpt.com "OECD global long-run economic scenarios: 2025 update" +[5]: https://www.theguardian.com/world/2025/nov/27/jakarta-overtakes-tokyo-most-populous-city-world?utm_source=chatgpt.com "Jakarta overtakes Tokyo as world's most populous city, according to UN" +[6]: https://www.imf.org/en/publications/weo?utm_source=chatgpt.com "World Economic Outlook - All Issues" +[7]: https://www.weforum.org/publications/global-risks-report-2025/digest/?utm_source=chatgpt.com "Global Risks Report 2025 | World Economic Forum" +[8]: https://wmo.int/publication-series/state-of-global-climate-2024?utm_source=chatgpt.com "State of the Global Climate 2024" +[9]: https://www.un.org/en/climatechange/reports?utm_source=chatgpt.com "Climate Reports" +[10]: https://www.theguardian.com/environment/2025/mar/19/unprecedented-climate-disasters-extreme-weather-un-report?utm_source=chatgpt.com "More than 150 'unprecedented' climate disasters struck world in 2024, says UN" +[11]: https://www.who.int/news/item/29-10-2025-climate-inaction-is-claiming-millions-of-lives-every-year--warns-new-lancet-countdown-report?utm_source=chatgpt.com "Climate inaction is claiming millions of lives every year, ..." +[12]: https://www.un.org/en/global-issues?utm_source=chatgpt.com "Global Issues" +[13]: https://www.climaterealityproject.org/blog/how-climate-crisis-impacting-bangladesh?utm_source=chatgpt.com "How the Climate Crisis Is Impacting Bangladesh" diff --git a/test/test_lightrag.py b/test/test_lightrag.py new file mode 100644 index 0000000..2ea2d1f --- /dev/null +++ b/test/test_lightrag.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Simple standalone LightRAG test - NO RunAgent needed +""" +import os +import asyncio +import shutil +from lightrag import LightRAG, QueryParam +from lightrag.llm.openai import gpt_4o_mini_complete, openai_embed +from lightrag.kg.shared_storage import initialize_pipeline_status + +WORKING_DIR = "prova/test_rag_storage" + +async def main(): + # Step 1: Clear old data to start fresh + print("\n" + "="*60) + print("STEP 1: Clearing old data") + print("="*60) + if os.path.exists(WORKING_DIR): + shutil.rmtree(WORKING_DIR) + print(f"βœ… Cleared {WORKING_DIR}") + else: + print(f"ℹ️ No existing data") + + # Step 2: Create RAG instance + print("\n" + "="*60) + print("STEP 2: Initializing LightRAG") + print("="*60) + + rag = LightRAG( + working_dir=WORKING_DIR, + embedding_func=openai_embed, + llm_model_func=gpt_4o_mini_complete, + ) + + await rag.initialize_storages() + await initialize_pipeline_status() # Add this line! + print("βœ… Storage initialized") + + # Step 3: Insert text + print("\n" + "="*60) + print("STEP 3: Inserting text") + print("="*60) + + sample_text = """ + The global economy showed signs of recovery in 2024. + Major indicators including GDP growth, employment rates, and consumer spending + all demonstrated positive trends. Economists predict continued growth + through 2025, driven by technological innovation and sustainable practices. + """ + + print(f"Text length: {len(sample_text)} characters") + print("Starting insertion (this may take a minute for entity extraction)...") + + await rag.ainsert(sample_text) + + print("βœ… Text inserted and processed") + + # Step 4: Check what was created + print("\n" + "="*60) + print("STEP 4: Checking created files") + print("="*60) + + files = os.listdir(WORKING_DIR) + for f in sorted(files): + path = os.path.join(WORKING_DIR, f) + size = os.path.getsize(path) + print(f" πŸ“„ {f}: {size} bytes") + + # Step 5: Query + print("\n" + "="*60) + print("STEP 5: Querying") + print("="*60) + + result = await rag.aquery( + "What are the economic trends?", + param=QueryParam(mode="hybrid") + ) + + print(f"\nπŸ“Š Query Result:\n{result}\n") + + # Cleanup + await rag.finalize_storages() + + print("="*60) + print("βœ… Test completed successfully!") + print("="*60) + +if __name__ == "__main__": + # Make sure you have OPENAI_API_KEY set + if not os.getenv("OPENAI_API_KEY"): + print("❌ Error: OPENAI_API_KEY not set!") + print("Set it with: export OPENAI_API_KEY='sk-...'") + exit(1) + + asyncio.run(main()) \ No newline at end of file diff --git a/test_scripts/python/client_test_agno.py b/test_scripts/python/client_test_agno.py index c77ff38..5710021 100644 --- a/test_scripts/python/client_test_agno.py +++ b/test_scripts/python/client_test_agno.py @@ -17,7 +17,7 @@ from runagent import RunAgentClient ra = RunAgentClient( - agent_id="ae29bd73-b3d3-22c8-a98f-5d7aec7ee919", + agent_id="ae29bd73-b3d3-99c8-a98f-5d7aec7ee919", entrypoint_tag="agno_print_response_stream", local=False ) diff --git a/test_scripts/python/client_test_lightrag.py b/test_scripts/python/client_test_lightrag.py new file mode 100644 index 0000000..f597215 --- /dev/null +++ b/test_scripts/python/client_test_lightrag.py @@ -0,0 +1,61 @@ +from runagent import RunAgentClient +import os + +# Configuration +AGENT_ID = "63751c14-0ed5-426c-ab44-aa94e3505bed" +LOCAL_MODE = False +USER_ID = "rad123" + +# Initialize clients +ingest_client = RunAgentClient( + agent_id=AGENT_ID, + entrypoint_tag="ingest_text", + local=LOCAL_MODE, + user_id=USER_ID, + persistent_memory=True +) + +query_client = RunAgentClient( + agent_id=AGENT_ID, + entrypoint_tag="query_rag", + local=LOCAL_MODE, + user_id=USER_ID, + persistent_memory=True +) + + +def ingest_from_file(file_path: str): + if not os.path.exists(file_path): + print(f"Error: File not found: {file_path}") + return None + + with open(file_path, 'r', encoding='utf-8') as f: + text = f.read() + + print(f"Ingesting {len(text)} characters...") + result = ingest_client.run(text=text) + + return result + + +def query_rag(question: str, mode: str = "hybrid"): + print(f"\nQuerying: {question}") + result = query_client.run(query=question, mode=mode) + + return result + + +if __name__ == "__main__": + # Step 1: Ingest text + print("="*60) + print("STEP 1: Ingest Document") + print("="*60) + # ingest_from_file("/home/azureuser/runagent/test/rag_test.txt") + + # # Step 2: Query + # print("\n" + "="*60) + # print("STEP 2: Query RAG") + # print("="*60) + print(query_rag("how much world population right now in 2025", mode="hybrid")) + + # print("\nDone!") \ No newline at end of file From cdfc45eb81b45ed3b8c92905e3d4ec469048088d Mon Sep 17 00:00:00 2001 From: RadeenXALNW Date: Sun, 30 Nov 2025 11:02:43 +0000 Subject: [PATCH 4/8] test with lightrag --- examples/lightrag_persistent/runagent.config.json | 2 +- test_scripts/python/client_test_lightrag.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/lightrag_persistent/runagent.config.json b/examples/lightrag_persistent/runagent.config.json index 3fad432..2a17998 100644 --- a/examples/lightrag_persistent/runagent.config.json +++ b/examples/lightrag_persistent/runagent.config.json @@ -25,7 +25,7 @@ ] }, "env_vars": {}, - "agent_id": "63751c14-0ed5-426c-ab44-aa94e3505bed", + "agent_id": "63751c14-0ed5-426c-ab44-aa94e5505bed", "auth_settings": { "type": "api_key" }, diff --git a/test_scripts/python/client_test_lightrag.py b/test_scripts/python/client_test_lightrag.py index f597215..ab6575d 100644 --- a/test_scripts/python/client_test_lightrag.py +++ b/test_scripts/python/client_test_lightrag.py @@ -2,7 +2,7 @@ import os # Configuration -AGENT_ID = "63751c14-0ed5-426c-ab44-aa94e3505bed" +AGENT_ID = "63751c14-0ed5-426c-ab44-aa94e5505bed" LOCAL_MODE = False USER_ID = "rad123" @@ -56,6 +56,6 @@ def query_rag(question: str, mode: str = "hybrid"): # print("\n" + "="*60) # print("STEP 2: Query RAG") # print("="*60) - print(query_rag("how much world population right now in 2025", mode="hybrid")) + print(query_rag("globalization with megacity", mode="hybrid")) # print("\nDone!") \ No newline at end of file From f1e2e52677e38e378fd353497b726702579474f5 Mon Sep 17 00:00:00 2001 From: RadeenXALNW Date: Thu, 4 Dec 2025 11:04:13 +0000 Subject: [PATCH 5/8] added dart and rust sdk for persistent feature --- runagent-dart/lib/src/client/rest_client.dart | 12 +- .../lib/src/client/runagent_client.dart | 10 ++ .../lib/src/client/socket_client.dart | 12 +- runagent-dart/lib/src/types/types.dart | 12 ++ runagent-rust/runagent/src/blocking.rs | 10 ++ .../runagent/src/client/rest_client.rs | 19 ++- .../runagent/src/client/runagent_client.rs | 41 ++++++ .../runagent/src/client/socket_client.rs | 19 ++- runagent/cli/commands/run.py | 17 ++- runagent/cli/commands/run_stream.py | 17 ++- .../.dart_tool/package_config.json | 106 +++++++++++++++ .../.dart_tool/package_graph.json | 124 ++++++++++++++++++ test_scripts/dart/test_lightrag/lib/main.dart | 86 ++++++++++++ test_scripts/dart/test_lightrag/pubspec.yaml | 11 ++ test_scripts/rust/test_lightrag/Cargo.toml | 16 +++ test_scripts/rust/test_lightrag/src/main.rs | 59 +++++++++ 16 files changed, 563 insertions(+), 8 deletions(-) create mode 100644 test_scripts/dart/test_lightrag/.dart_tool/package_config.json create mode 100644 test_scripts/dart/test_lightrag/.dart_tool/package_graph.json create mode 100644 test_scripts/dart/test_lightrag/lib/main.dart create mode 100644 test_scripts/dart/test_lightrag/pubspec.yaml create mode 100644 test_scripts/rust/test_lightrag/Cargo.toml create mode 100644 test_scripts/rust/test_lightrag/src/main.rs diff --git a/runagent-dart/lib/src/client/rest_client.dart b/runagent-dart/lib/src/client/rest_client.dart index dfdbe4d..97b460f 100644 --- a/runagent-dart/lib/src/client/rest_client.dart +++ b/runagent-dart/lib/src/client/rest_client.dart @@ -55,6 +55,8 @@ class RestClient { Map inputKwargs, { int timeoutSeconds = 300, bool asyncExecution = false, + String? userId, + bool persistentMemory = false, }) async { final url = Uri.parse('$baseUrl${RunAgentConstants.defaultApiPrefix}/agents/$agentId/run'); @@ -72,7 +74,7 @@ class RestClient { ); } - final payload = { + final payload = { 'entrypoint_tag': entrypointTag, 'input_args': inputArgs, 'input_kwargs': inputKwargs, @@ -80,6 +82,14 @@ class RestClient { 'async_execution': asyncExecution, }; + // Add persistent storage parameters if provided (matches Python SDK) + if (userId != null) { + payload['user_id'] = userId; + } + if (persistentMemory) { + payload['persistent_memory'] = persistentMemory; + } + try { final response = await http.post( url, diff --git a/runagent-dart/lib/src/client/runagent_client.dart b/runagent-dart/lib/src/client/runagent_client.dart index 485478c..147517b 100644 --- a/runagent-dart/lib/src/client/runagent_client.dart +++ b/runagent-dart/lib/src/client/runagent_client.dart @@ -15,6 +15,8 @@ class RunAgentClient { final RestClient restClient; final SocketClient socketClient; final Map? extraParams; + final String? userId; + final bool persistentMemory; AgentArchitecture? _architecture; @@ -25,6 +27,8 @@ class RunAgentClient { required this.restClient, required this.socketClient, this.extraParams, + this.userId, + this.persistentMemory = false, }); /// Create a new RunAgent client from configuration @@ -110,6 +114,8 @@ class RunAgentClient { restClient: restClient, socketClient: socketClient, extraParams: config.extraParams, + userId: config.userId, + persistentMemory: config.persistentMemory ?? false, ); // Initialize architecture and validate entrypoint @@ -265,6 +271,8 @@ class RunAgentClient { entrypointTag, inputArgs, inputKwargs, + userId: userId, + persistentMemory: persistentMemory, ); if (response['success'] == true) { @@ -369,6 +377,8 @@ class RunAgentClient { entrypointTag, inputArgs, inputKwargs, + userId: userId, + persistentMemory: persistentMemory, ); } diff --git a/runagent-dart/lib/src/client/socket_client.dart b/runagent-dart/lib/src/client/socket_client.dart index 958c7e4..556a6cf 100644 --- a/runagent-dart/lib/src/client/socket_client.dart +++ b/runagent-dart/lib/src/client/socket_client.dart @@ -23,6 +23,8 @@ class SocketClient { List inputArgs, Map inputKwargs, { int timeoutSeconds = 600, + String? userId, + bool persistentMemory = false, }) async* { // Build WebSocket URL String uri; @@ -58,13 +60,21 @@ class SocketClient { ); // Send initial request - final requestData = { + final requestData = { 'entrypoint_tag': entrypointTag, 'input_args': inputArgs, 'input_kwargs': inputKwargs, 'timeout_seconds': timeoutSeconds, }; + // Add persistent storage parameters if provided (matches Python SDK) + if (userId != null) { + requestData['user_id'] = userId; + } + if (persistentMemory) { + requestData['persistent_memory'] = persistentMemory; + } + channel.sink.add(jsonEncode(requestData)); // Stream responses diff --git a/runagent-dart/lib/src/types/types.dart b/runagent-dart/lib/src/types/types.dart index 00a378d..a20d9c5 100644 --- a/runagent-dart/lib/src/types/types.dart +++ b/runagent-dart/lib/src/types/types.dart @@ -29,6 +29,12 @@ class RunAgentClientConfig { /// Enable database registry lookup (default: true for local agents) final bool? enableRegistry; + /// User ID for persistent memory (matches Python SDK RunAgentClient.user_id) + final String? userId; + + /// Enable persistent memory for this user (matches Python SDK RunAgentClient.persistent_memory) + final bool? persistentMemory; + RunAgentClientConfig({ required this.agentId, required this.entrypointTag, @@ -39,6 +45,8 @@ class RunAgentClientConfig { this.baseUrl, this.extraParams, this.enableRegistry, + this.userId, + this.persistentMemory, }); /// Create a config with required fields @@ -52,6 +60,8 @@ class RunAgentClientConfig { String? baseUrl, Map? extraParams, bool? enableRegistry, + String? userId, + bool? persistentMemory, }) { return RunAgentClientConfig( agentId: agentId, @@ -63,6 +73,8 @@ class RunAgentClientConfig { baseUrl: baseUrl, extraParams: extraParams, enableRegistry: enableRegistry, + userId: userId, + persistentMemory: persistentMemory, ); } } diff --git a/runagent-rust/runagent/src/blocking.rs b/runagent-rust/runagent/src/blocking.rs index 3dca1a4..d465cea 100644 --- a/runagent-rust/runagent/src/blocking.rs +++ b/runagent-rust/runagent/src/blocking.rs @@ -144,6 +144,16 @@ impl RunAgentClient { self.inner.extra_params() } + /// Get user ID used for persistent memory, if any + pub fn user_id(&self) -> Option<&str> { + self.inner.user_id() + } + + /// Check if persistent memory is enabled for this client + pub fn persistent_memory(&self) -> bool { + self.inner.persistent_memory() + } + /// Check if this is a local client pub fn is_local(&self) -> bool { self.inner.is_local() diff --git a/runagent-rust/runagent/src/client/rest_client.rs b/runagent-rust/runagent/src/client/rest_client.rs index 8a788a4..d0ece10 100644 --- a/runagent-rust/runagent/src/client/rest_client.rs +++ b/runagent-rust/runagent/src/client/rest_client.rs @@ -190,8 +190,10 @@ impl RestClient { entrypoint_tag: &str, input_args: &[Value], input_kwargs: &HashMap, + user_id: Option<&str>, + persistent_memory: bool, ) -> RunAgentResult { - let data = serde_json::json!({ + let mut data = serde_json::json!({ "id": "run_start", "entrypoint_tag": entrypoint_tag, "input_args": input_args, @@ -200,6 +202,21 @@ impl RestClient { "async_execution": false }); + // Add persistent storage parameters if provided (matches Python SDK) + if let Some(uid) = user_id { + if let Some(obj) = data.as_object_mut() { + obj.insert("user_id".to_string(), serde_json::json!(uid)); + } + } + if persistent_memory { + if let Some(obj) = data.as_object_mut() { + obj.insert( + "persistent_memory".to_string(), + serde_json::json!(persistent_memory), + ); + } + } + let path = format!("agents/{}/run", agent_id); let url = self.get_url(&path)?; tracing::debug!( diff --git a/runagent-rust/runagent/src/client/runagent_client.rs b/runagent-rust/runagent/src/client/runagent_client.rs index 4b50d29..7c4efcf 100644 --- a/runagent-rust/runagent/src/client/runagent_client.rs +++ b/runagent-rust/runagent/src/client/runagent_client.rs @@ -23,6 +23,11 @@ pub struct RunAgentClient { agent_architecture: Option, extra_params: Option>, + /// User ID for persistent memory (matches Python SDK RunAgentClient.user_id) + user_id: Option, + /// Enable persistent memory for this user (matches Python SDK RunAgentClient.persistent_memory) + persistent_memory: bool, + #[cfg(feature = "db")] #[allow(dead_code)] // Reserved for future use db_service: Option, @@ -90,6 +95,10 @@ pub struct RunAgentClientConfig { pub extra_params: Option>, /// Enable database registry lookup (default: true for local agents) pub enable_registry: Option, + /// User ID for persistent memory + pub user_id: Option, + /// Enable persistent memory for this user + pub persistent_memory: Option, } #[allow(clippy::derivable_impls)] @@ -105,6 +114,8 @@ impl Default for RunAgentClientConfig { base_url: None, extra_params: None, enable_registry: None, + user_id: None, + persistent_memory: None, } } } @@ -122,6 +133,8 @@ impl RunAgentClientConfig { base_url: None, extra_params: None, enable_registry: None, + user_id: None, + persistent_memory: None, } } @@ -161,6 +174,18 @@ impl RunAgentClientConfig { self.enable_registry = Some(enable); self } + + /// Set user ID for persistent memory + pub fn with_user_id(mut self, user_id: impl Into) -> Self { + self.user_id = Some(user_id.into()); + self + } + + /// Enable or disable persistent memory for this user + pub fn with_persistent_memory(mut self, persistent: bool) -> Self { + self.persistent_memory = Some(persistent); + self + } } impl RunAgentClient { @@ -288,6 +313,8 @@ impl RunAgentClient { serializer, agent_architecture: None, extra_params: config.extra_params, + user_id: config.user_id, + persistent_memory: config.persistent_memory.unwrap_or(false), #[cfg(feature = "db")] db_service, @@ -372,6 +399,8 @@ impl RunAgentClient { &self.entrypoint_tag, input_args, &input_kwargs_map, + self.user_id.as_deref(), + self.persistent_memory, ) .await?; @@ -521,6 +550,8 @@ impl RunAgentClient { &self.entrypoint_tag, input_args, &input_kwargs_map, + self.user_id.as_deref(), + self.persistent_memory, ) .await } @@ -555,6 +586,16 @@ impl RunAgentClient { self.extra_params.as_ref() } + /// Get user ID used for persistent memory, if any + pub fn user_id(&self) -> Option<&str> { + self.user_id.as_deref() + } + + /// Check if persistent memory is enabled for this client + pub fn persistent_memory(&self) -> bool { + self.persistent_memory + } + /// Check if using local deployment pub fn is_local(&self) -> bool { self.local diff --git a/runagent-rust/runagent/src/client/socket_client.rs b/runagent-rust/runagent/src/client/socket_client.rs index b8690a6..f82aea7 100644 --- a/runagent-rust/runagent/src/client/socket_client.rs +++ b/runagent-rust/runagent/src/client/socket_client.rs @@ -73,6 +73,8 @@ impl SocketClient { entrypoint_tag: &str, input_args: &[Value], input_kwargs: &HashMap, + user_id: Option<&str>, + persistent_memory: bool, ) -> RunAgentResult> + Send>>> { let url = self.get_websocket_url(agent_id, entrypoint_tag)?; @@ -86,7 +88,7 @@ impl SocketClient { let (mut write, mut read) = ws_stream.split(); // Prepare start stream request with id field (as middleware expects) - let request_data = serde_json::json!({ + let mut request_data = serde_json::json!({ "id": "stream_start", "entrypoint_tag": entrypoint_tag, "input_args": input_args, @@ -95,6 +97,21 @@ impl SocketClient { "async_execution": false }); + // Add persistent storage parameters if provided (matches Python SDK) + if let Some(uid) = user_id { + if let Some(obj) = request_data.as_object_mut() { + obj.insert("user_id".to_string(), serde_json::json!(uid)); + } + } + if persistent_memory { + if let Some(obj) = request_data.as_object_mut() { + obj.insert( + "persistent_memory".to_string(), + serde_json::json!(persistent_memory), + ); + } + } + // Send the request data directly (matching Python SDK format) let serialized_msg = serde_json::to_string(&request_data)?; write diff --git a/runagent/cli/commands/run.py b/runagent/cli/commands/run.py index d1e91e8..acbbfb1 100644 --- a/runagent/cli/commands/run.py +++ b/runagent/cli/commands/run.py @@ -64,8 +64,19 @@ def format_error_message(error_info): @click.option("--tag", required=True, help="Entrypoint tag to be used") # @click.option("--generic-stream", is_flag=True, help="Use generic streaming mode") @click.option("--timeout", type=int, help="Timeout in seconds") +@click.option( + "--user-id", + type=str, + help="User ID for persistent memory (matches SDK RunAgentClient.user_id)", +) +@click.option( + "--persistent-memory", + is_flag=True, + default=False, + help="Enable persistent memory for the given user_id", +) @click.pass_context -def run(ctx, agent_id, host, port, input_file, local, tag, timeout): +def run(ctx, agent_id, host, port, input_file, local, tag, timeout, user_id, persistent_memory): """ Run an agent with flexible configuration options @@ -215,7 +226,9 @@ def run(ctx, agent_id, host, port, input_file, local, tag, timeout): local=local, host=host, port=port, - entrypoint_tag=tag + entrypoint_tag=tag, + user_id=user_id, + persistent_memory=persistent_memory, ) if tag.endswith("_stream"): diff --git a/runagent/cli/commands/run_stream.py b/runagent/cli/commands/run_stream.py index da92bf1..31ac577 100644 --- a/runagent/cli/commands/run_stream.py +++ b/runagent/cli/commands/run_stream.py @@ -62,8 +62,19 @@ def format_error_message(error_info): @click.option("--local", is_flag=True, help="Run agent locally") @click.option("--tag", required=True, help="Entrypoint tag to be used") @click.option("--timeout", type=int, help="Timeout in seconds") +@click.option( + "--user-id", + type=str, + help="User ID for persistent memory (matches SDK RunAgentClient.user_id)", +) +@click.option( + "--persistent-memory", + is_flag=True, + default=False, + help="Enable persistent memory for the given user_id", +) @click.pass_context -def run_stream(ctx, agent_id, host, port, input_file, local, tag, timeout): +def run_stream(ctx, agent_id, host, port, input_file, local, tag, timeout, user_id, persistent_memory): """ Stream agent execution results in real-time. @@ -182,7 +193,9 @@ def run_stream(ctx, agent_id, host, port, input_file, local, tag, timeout): local=local, host=host, port=port, - entrypoint_tag=tag + entrypoint_tag=tag, + user_id=user_id, + persistent_memory=persistent_memory, ) console.print(f"\n[bold]Starting streaming execution...[/bold]") diff --git a/test_scripts/dart/test_lightrag/.dart_tool/package_config.json b/test_scripts/dart/test_lightrag/.dart_tool/package_config.json new file mode 100644 index 0000000..d223906 --- /dev/null +++ b/test_scripts/dart/test_lightrag/.dart_tool/package_config.json @@ -0,0 +1,106 @@ +{ + "configVersion": 2, + "packages": [ + { + "name": "async", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/async-2.13.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "collection", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/collection-1.19.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "crypto", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/crypto-3.0.7", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "http", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/http-1.6.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "http_parser", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/http_parser-4.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "meta", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/meta-1.17.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "path", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/path-1.9.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "runagent", + "rootUri": "file:///home/azureuser/runagent/runagent-dart", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "source_span", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/source_span-1.10.1", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "stream_channel", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/stream_channel-2.1.4", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "string_scanner", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/string_scanner-1.4.1", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "term_glyph", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/term_glyph-1.2.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "typed_data", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/typed_data-1.4.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "web", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/web-0.5.1", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "web_socket_channel", + "rootUri": "file:///home/azureuser/.pub-cache/hosted/pub.dev/web_socket_channel-2.4.5", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "test_lightrag", + "rootUri": "../", + "packageUri": "lib/", + "languageVersion": "3.0" + } + ], + "generator": "pub", + "generatorVersion": "3.10.0", + "flutterRoot": "file:///home/azureuser/snap/flutter/common/flutter", + "flutterVersion": "3.38.2", + "pubCache": "file:///home/azureuser/.pub-cache" +} diff --git a/test_scripts/dart/test_lightrag/.dart_tool/package_graph.json b/test_scripts/dart/test_lightrag/.dart_tool/package_graph.json new file mode 100644 index 0000000..a97b7fb --- /dev/null +++ b/test_scripts/dart/test_lightrag/.dart_tool/package_graph.json @@ -0,0 +1,124 @@ +{ + "roots": [ + "test_lightrag" + ], + "packages": [ + { + "name": "test_lightrag", + "version": "0.1.0", + "dependencies": [ + "runagent" + ], + "devDependencies": [] + }, + { + "name": "runagent", + "version": "0.1.41", + "dependencies": [ + "http", + "web_socket_channel" + ] + }, + { + "name": "web_socket_channel", + "version": "2.4.5", + "dependencies": [ + "async", + "crypto", + "stream_channel", + "web" + ] + }, + { + "name": "http", + "version": "1.6.0", + "dependencies": [ + "async", + "http_parser", + "meta", + "web" + ] + }, + { + "name": "web", + "version": "0.5.1", + "dependencies": [] + }, + { + "name": "stream_channel", + "version": "2.1.4", + "dependencies": [ + "async" + ] + }, + { + "name": "crypto", + "version": "3.0.7", + "dependencies": [ + "typed_data" + ] + }, + { + "name": "async", + "version": "2.13.0", + "dependencies": [ + "collection", + "meta" + ] + }, + { + "name": "meta", + "version": "1.17.0", + "dependencies": [] + }, + { + "name": "http_parser", + "version": "4.1.2", + "dependencies": [ + "collection", + "source_span", + "string_scanner", + "typed_data" + ] + }, + { + "name": "typed_data", + "version": "1.4.0", + "dependencies": [ + "collection" + ] + }, + { + "name": "collection", + "version": "1.19.1", + "dependencies": [] + }, + { + "name": "string_scanner", + "version": "1.4.1", + "dependencies": [ + "source_span" + ] + }, + { + "name": "source_span", + "version": "1.10.1", + "dependencies": [ + "collection", + "path", + "term_glyph" + ] + }, + { + "name": "term_glyph", + "version": "1.2.2", + "dependencies": [] + }, + { + "name": "path", + "version": "1.9.1", + "dependencies": [] + } + ], + "configVersion": 1 +} \ No newline at end of file diff --git a/test_scripts/dart/test_lightrag/lib/main.dart b/test_scripts/dart/test_lightrag/lib/main.dart new file mode 100644 index 0000000..220c79f --- /dev/null +++ b/test_scripts/dart/test_lightrag/lib/main.dart @@ -0,0 +1,86 @@ +import 'dart:io'; +import 'package:runagent/runagent.dart'; + +// Configuration (mirrors test_scripts/python/client_test_lightrag.py) +const String agentId = '63751c14-0ed5-426c-ab44-aa94e5505bed'; +const bool localMode = false; +const String userId = 'rad123'; + +/// Ingest text from a file +Future ingestFromFile(String filePath) async { + final file = File(filePath); + if (!await file.exists()) { + print('Error: File not found: $filePath'); + return null; + } + + final text = await file.readAsString(); + print('Ingesting ${text.length} characters...'); + + final ingestClient = await RunAgentClient.create( + RunAgentClientConfig.create( + agentId: agentId, + entrypointTag: 'ingest_text', + local: localMode, + userId: userId, + persistentMemory: true, + ), + ); + + final result = await ingestClient.run({'text': text}); + return result; +} + +/// Query the RAG system +Future queryRag(String question, {String mode = 'hybrid'}) async { + print('\nQuerying: $question'); + + final queryClient = await RunAgentClient.create( + RunAgentClientConfig.create( + agentId: agentId, + entrypointTag: 'query_rag', + local: localMode, + userId: userId, + persistentMemory: true, + ), + ); + + final result = await queryClient.run({ + 'query': question, + 'mode': mode, + }); + + return result; +} + +Future main() async { + try { + // Step 1: Ingest text (commented out, same as Python test) + // print('============================================================'); + // print('STEP 1: Ingest Document'); + // print('============================================================'); + // await ingestFromFile('/home/azureuser/runagent/test/rag_test.txt'); + + // Step 2: Query + print('============================================================'); + print('STEP 2: Query RAG'); + print('============================================================'); + + final result = await queryRag('population prediction', mode: 'hybrid'); + print(result); + + // print('\nDone!'); + } catch (e) { + if (e is RunAgentError) { + print('Error: ${e.message}'); + if (e.suggestion != null) { + print('Suggestion: ${e.suggestion}'); + } + exit(1); + } else { + print('Unexpected error: $e'); + exit(1); + } + } +} + diff --git a/test_scripts/dart/test_lightrag/pubspec.yaml b/test_scripts/dart/test_lightrag/pubspec.yaml new file mode 100644 index 0000000..6a96931 --- /dev/null +++ b/test_scripts/dart/test_lightrag/pubspec.yaml @@ -0,0 +1,11 @@ +name: test_lightrag +description: Test script for RunAgent Dart SDK with LightRAG agent +version: 0.1.0 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + runagent: + path: /home/azureuser/runagent/runagent-dart + diff --git a/test_scripts/rust/test_lightrag/Cargo.toml b/test_scripts/rust/test_lightrag/Cargo.toml new file mode 100644 index 0000000..2aadc39 --- /dev/null +++ b/test_scripts/rust/test_lightrag/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "temp_rust_lightrag_test" +version = "0.1.0" +edition = "2021" + +[dependencies] +# RunAgent Rust SDK from local path +runagent = { path = "/home/azureuser/runagent/runagent-rust/runagent" } + +# Required dependencies +tokio = { version = "1.0", features = ["full"] } +serde_json = "1.0" +anyhow = "1.0" +futures = "0.3" + + diff --git a/test_scripts/rust/test_lightrag/src/main.rs b/test_scripts/rust/test_lightrag/src/main.rs new file mode 100644 index 0000000..ea9223e --- /dev/null +++ b/test_scripts/rust/test_lightrag/src/main.rs @@ -0,0 +1,59 @@ +use runagent::{RunAgentClient, RunAgentClientConfig}; +use serde_json::json; + +// Configuration (mirrors test_scripts/python/client_test_lightrag.py) +const AGENT_ID: &str = "63751c14-0ed5-426c-ab44-aa94e5505bed"; +const LOCAL_MODE: bool = false; +const USER_ID: &str = "rad123"; + +#[tokio::main] +async fn main() -> runagent::RunAgentResult<()> { + // Ingest client (persistent memory enabled) + let ingest_client = RunAgentClient::new(RunAgentClientConfig { + agent_id: AGENT_ID.to_string(), + entrypoint_tag: "ingest_text".to_string(), + local: Some(LOCAL_MODE), + host: None, + port: None, + api_key: None, + base_url: None, + extra_params: None, + enable_registry: None, + user_id: Some(USER_ID.to_string()), + persistent_memory: Some(true), + }) + .await?; + + // Query client (same user_id + persistent_memory) + let query_client = RunAgentClient::new(RunAgentClientConfig { + agent_id: AGENT_ID.to_string(), + entrypoint_tag: "query_rag".to_string(), + local: Some(LOCAL_MODE), + host: None, + port: None, + api_key: None, + base_url: None, + extra_params: None, + enable_registry: None, + user_id: Some(USER_ID.to_string()), + persistent_memory: Some(true), + }) + .await?; + + // Example query (same as Python test) + let question = "population prediction"; + println!("============================================================"); + println!("STEP: Query RAG"); + println!("============================================================"); + + let result = query_client + .run(&[ + ("query", json!(question)), + // mode is optional; default is handled server-side but we match Python example + ("mode", json!("hybrid")), + ]) + .await?; + + println!("Result: {}", result); + Ok(()) +} From 51107f46ac4334b897347b4be1327e295fff1092 Mon Sep 17 00:00:00 2001 From: RadeenXALNW Date: Thu, 4 Dec 2025 11:07:28 +0000 Subject: [PATCH 6/8] dart package release --- runagent-dart/.dart_tool/package_graph.json | 2 +- runagent-dart/CHANGELOG.md | 12 ++++++++++++ runagent-dart/pubspec.yaml | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/runagent-dart/.dart_tool/package_graph.json b/runagent-dart/.dart_tool/package_graph.json index 1b63185..d62550d 100644 --- a/runagent-dart/.dart_tool/package_graph.json +++ b/runagent-dart/.dart_tool/package_graph.json @@ -5,7 +5,7 @@ "packages": [ { "name": "runagent", - "version": "0.1.41", + "version": "0.1.43", "dependencies": [ "http", "web_socket_channel" diff --git a/runagent-dart/CHANGELOG.md b/runagent-dart/CHANGELOG.md index 61f781f..cc554ce 100644 --- a/runagent-dart/CHANGELOG.md +++ b/runagent-dart/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.43] - 2024-XX-XX + +### Added +- Support for `userId` parameter in `RunAgentClientConfig` for persistent storage +- Support for `persistentMemory` parameter in `RunAgentClientConfig` to enable persistent memory across agent executions +- `userId` and `persistentMemory` are now passed through REST and WebSocket clients to the backend API + +### Changed +- `RunAgentClientConfig` now includes optional `userId` and `persistentMemory` fields +- `RestClient.runAgent()` now accepts and forwards `userId` and `persistentMemory` parameters +- `SocketClient.runStream()` now accepts and forwards `userId` and `persistentMemory` parameters + ## [0.1.41] - 2024-XX-XX ### Added diff --git a/runagent-dart/pubspec.yaml b/runagent-dart/pubspec.yaml index f58b819..13230d2 100644 --- a/runagent-dart/pubspec.yaml +++ b/runagent-dart/pubspec.yaml @@ -1,6 +1,6 @@ name: runagent description: RunAgent SDK for Dart/Flutter - Interact with deployed AI agents via REST and WebSocket -version: 0.1.41 +version: 0.1.43 homepage: https://github.com/runagent-dev/runagent-dart environment: From a0c2a740062bcda49a117e8bd51c46fa709b39b4 Mon Sep 17 00:00:00 2001 From: RadeenXALNW Date: Thu, 4 Dec 2025 11:37:45 +0000 Subject: [PATCH 7/8] Release runagent rust v0.1.42 --- runagent-rust/runagent/Cargo.toml | 2 +- runagent-rust/runagent/README.md | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/runagent-rust/runagent/Cargo.toml b/runagent-rust/runagent/Cargo.toml index aee3c59..fb20ea3 100644 --- a/runagent-rust/runagent/Cargo.toml +++ b/runagent-rust/runagent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runagent" -version = "0.1.41" +version = "0.1.42" edition = "2021" description = "RunAgent SDK for Rust - Client SDK for interacting with deployed AI agents" license = "MIT" diff --git a/runagent-rust/runagent/README.md b/runagent-rust/runagent/README.md index bfca33a..a256977 100644 --- a/runagent-rust/runagent/README.md +++ b/runagent-rust/runagent/README.md @@ -44,6 +44,14 @@ let client = RunAgentClient::new( RunAgentClientConfig::new("agent-id", "entrypoint") .with_api_key(env::var("RUNAGENT_API_KEY").unwrap()) ).await?; + +// Remote agent with persistent memory +let client = RunAgentClient::new( + RunAgentClientConfig::new("agent-id", "entrypoint") + .with_api_key(env::var("RUNAGENT_API_KEY").unwrap()) + .with_user_id("user123") + .with_persistent_memory(true) +).await?; ``` | Setting | Cloud | Local (auto discovery) | Local (explicit) | @@ -53,9 +61,13 @@ let client = RunAgentClient::new( | Base URL | `RUNAGENT_BASE_URL` \|\| default | n/a | n/a | | API Key | `RUNAGENT_API_KEY` (required) | optional | optional | | Registry | n/a | `true` (default) | `false` | +| `user_id` | optional | optional | optional | +| `persistent_memory` | optional (`false` default) | optional (`false` default) | optional (`false` default) | - `RUNAGENT_API_KEY`: Bearer token for remote agents (can be set via env var or `with_api_key()`). - `RUNAGENT_BASE_URL`: Override the default cloud endpoint (e.g. staging). +- `user_id`: Optional user identifier for persistent storage across agent executions. +- `persistent_memory`: Enable persistent memory to maintain state across multiple agent calls (default: `false`). - For local discovery, install the crate with the `db` feature and ensure the CLI has registered the agent in `~/.runagent/runagent_local.db`. --- @@ -260,6 +272,8 @@ During initialization the client calls `/api/v1/agents/{id}/architecture` and ex | `.with_api_key(key)` | Set API key (overrides env var). | | `.with_base_url(url)` | Override default base URL. | | `.with_enable_registry(bool)` | Enable/disable database lookup (default: `true` for local). | +| `.with_user_id(user_id)` | Set user ID for persistent storage. | +| `.with_persistent_memory(bool)` | Enable persistent memory across executions (default: `false`). | | `.with_extra_params(params)` | Set extra parameters for future use. | ### Client Methods From 88897003633294c3dafc695fb9b7274d02eaf3af Mon Sep 17 00:00:00 2001 From: RadeenXALNW Date: Thu, 4 Dec 2025 11:41:39 +0000 Subject: [PATCH 8/8] updating readme --- README.md | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b9a1871..ea839f1 100644 --- a/README.md +++ b/README.md @@ -514,6 +514,105 @@ RunAgent Cloud provides: --- +## 🧠 Persistent Memory: Revolutionary Serverless Memory System + +RunAgent introduces **Persistent Memory** - the fastest serverless memory system for AI agents. Unlike traditional stateless serverless architectures, RunAgent enables your agents to maintain context and state across executions, creating truly intelligent and context-aware applications. + +### Why Persistent Memory Matters + +Traditional serverless functions are stateless by design, meaning each invocation starts fresh with no memory of previous interactions. RunAgent's Persistent Memory breaks this limitation, allowing your agents to: + +- **Remember Context** - Maintain conversation history and user preferences across sessions +- **Learn from Interactions** - Build upon previous executions to improve responses +- **Stateful Workflows** - Create multi-step processes that remember where they left off +- **Cross-Language Persistence** - Memory works seamlessly across all SDK languages (Python, JavaScript, Rust, Go, Dart) + +### How It Works + +Persistent Memory in RunAgent is designed for speed and reliability: + +```python +from runagent import RunAgentClient + +# Create a client with persistent memory enabled +client = RunAgentClient( + agent_id="my-agent-id", + entrypoint_tag="chat", + user_id="user123", # User identifier for memory isolation + persistent_memory=True # Enable persistent memory +) + +# First interaction - agent learns user preferences +result1 = client.run(message="I prefer dark mode interfaces") + +# Second interaction - agent remembers the preference +result2 = client.run(message="What's my UI preference?") +# Agent responds: "You prefer dark mode interfaces" +``` + +### Multi-Language Support + +Persistent Memory works identically across all SDKs: + +**Python:** +```python +client = RunAgentClient( + agent_id="agent-id", + entrypoint_tag="entrypoint", + user_id="user123", + persistent_memory=True +) +``` + +**JavaScript:** +```javascript +const client = new RunAgentClient({ + agentId: "agent-id", + entrypointTag: "entrypoint", + userId: "user123", + persistentMemory: true +}); +``` + +**Rust:** +```rust +let client = RunAgentClient::new( + RunAgentClientConfig::new("agent-id", "entrypoint") + .with_user_id("user123") + .with_persistent_memory(true) +).await?; +``` + +**Dart:** +```dart +final client = await RunAgentClient.create( + RunAgentClientConfig.create( + agentId: "agent-id", + entrypointTag: "entrypoint", + userId: "user123", + persistentMemory: true, + ), +); +``` + +### Key Benefits + +- ⚑ **Fastest Serverless Memory** - Optimized for low-latency access and updates +- πŸ”’ **Secure & Isolated** - Each `user_id` has isolated memory space +- 🌐 **Universal** - Works with any framework (LangGraph, CrewAI, Letta, etc.) +- πŸ“ˆ **Scalable** - Built on serverless infrastructure that scales automatically +- πŸ”„ **Stateful Workflows** - Enable complex multi-turn conversations and workflows + +### Use Cases + +- **Conversational AI** - Maintain context across multiple user interactions +- **Personalization** - Remember user preferences and adapt responses +- **Multi-Step Processes** - Track progress through complex workflows +- **Learning Systems** - Agents that improve based on interaction history +- **Session Management** - Maintain state across distributed systems + +--- + ## πŸ“š Documentation - **[Getting Started](https://docs.run-agent.ai/get-started/introduction.md)** - Deploy your first agent in 5 minutes @@ -527,7 +626,7 @@ RunAgent Cloud provides: ## 🧠 Action Memory System (Coming Soon) -RunAgent is introducing **Action Memory** - a revolutionary approach to agent reliability that focuses on *how to remember* rather than *what to remember*. +Building on our Persistent Memory foundation, RunAgent is introducing **Action Memory** - an advanced approach to agent reliability that focuses on *how to remember* rather than *what to remember*. ### How It Will Work @@ -536,7 +635,7 @@ RunAgent is introducing **Action Memory** - a revolutionary approach to agent re - **Reliability Focus**: Learns from successful outcomes to improve future decisions - **Ecosystem Integration**: Works with any framework - LangGraph, CrewAI, Letta, and more -This will ensure your agents become more reliable over time, regardless of which programming language or framework you use to interact with them. +This will ensure your agents become more reliable over time, building upon the Persistent Memory system to create truly intelligent, context-aware agents. ---