From bdb347932d1d22956d581c59a09011030de0c351 Mon Sep 17 00:00:00 2001 From: sawradip Date: Fri, 26 Sep 2025 02:01:57 +0600 Subject: [PATCH 1/4] feat: runagent cloud support - partial implementation --- CHANGELOG.md | 14 +- pyproject.toml | 6 +- release.sh | 0 runagent-go/runagent/version.go | 2 +- runagent-rust/runagent/Cargo.toml | 2 +- runagent-ts/package-lock.json | 4 +- runagent-ts/package.json | 2 +- runagent/__version__.py | 2 +- runagent/cli/commands.py | 175 ++-- runagent/sdk/deployment/remote.py | 6 +- runagent/sdk/rest_client.py | 945 +++++++++++----------- runagent/sdk/sdk.py | 12 - runagent/sdk/server/framework/__init__.py | 25 +- runagent/sdk/server/local_server.py | 10 +- runagent/utils/agent.py | 26 +- runagent/utils/enums.py | 26 - runagent/utils/schema.py | 25 +- 17 files changed, 632 insertions(+), 650 deletions(-) mode change 100644 => 100755 release.sh delete mode 100644 runagent/utils/enums.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 06cdae6..d972773 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,22 @@ # Changelog All notable changes to this project's latest version. -## [0.1.18] - 2025-07-29 +## [0.1.21] - 2025-09-23 ### Bug Fixes -- Fixed relase yml +- Fixed templatemanger checking all templates ### Features -- Realease action workflow fixed +- Added framework enum +- Framework enum support +- Agent upload working +- Restclient agent/start working with new response format -### Miscellaneous Tasks +### Refactor -- Bump version to v0.1.18 +- Disabled deploy-local +- Remove deply-local p2 diff --git a/pyproject.toml b/pyproject.toml index d54de53..b14138d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "runagent" -version = "0.1.19" +version = "0.1.21" description = "A command-line tool and SDK for deploying, managing, and interacting with AI agents" readme = "README.md" requires-python = ">=3.9" @@ -103,7 +103,7 @@ line_length = 88 skip = ["docs"] [tool.mypy] -python_version = "0.1.19" +python_version = "0.1.21" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true @@ -159,7 +159,7 @@ fail_under = 80 [tool.ruff] line-length = 88 -target-version = "0.1.19" +target-version = "0.1.21" select = [ "E", # pycodestyle errors "W", # pycodestyle warnings diff --git a/release.sh b/release.sh old mode 100644 new mode 100755 diff --git a/runagent-go/runagent/version.go b/runagent-go/runagent/version.go index 11d8ecc..158b463 100644 --- a/runagent-go/runagent/version.go +++ b/runagent-go/runagent/version.go @@ -1,4 +1,4 @@ package runagent // Version represents the current version of the RunAgent Go SDK -const Version = "0.1.19" +const Version = "0.1.21" diff --git a/runagent-rust/runagent/Cargo.toml b/runagent-rust/runagent/Cargo.toml index ed9059f..84ddee2 100644 --- a/runagent-rust/runagent/Cargo.toml +++ b/runagent-rust/runagent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runagent" -version = "0.1.19" +version = "0.1.21" edition = "2021" description = "RunAgent SDK for Rust - Deploy and manage AI agents easily" license = "MIT" diff --git a/runagent-ts/package-lock.json b/runagent-ts/package-lock.json index 680e17a..b69b847 100644 --- a/runagent-ts/package-lock.json +++ b/runagent-ts/package-lock.json @@ -1,12 +1,12 @@ { "name": "runagent", - "version": "0.1.19", + "version": "0.1.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "runagent", - "version": "0.1.19", + "version": "0.1.21", "dependencies": { "better-sqlite3": "^12.2.0" }, diff --git a/runagent-ts/package.json b/runagent-ts/package.json index 637870c..ad0f182 100644 --- a/runagent-ts/package.json +++ b/runagent-ts/package.json @@ -1,6 +1,6 @@ { "name": "runagent", - "version": "0.1.19", + "version": "0.1.21", "type": "module", "files": [ "dist" diff --git a/runagent/__version__.py b/runagent/__version__.py index d38c350..f4bd716 100644 --- a/runagent/__version__.py +++ b/runagent/__version__.py @@ -1 +1 @@ -__version__ = "0.1.19" +__version__ = "0.1.21" diff --git a/runagent/cli/commands.py b/runagent/cli/commands.py index c3dd4b6..d1cd2bc 100644 --- a/runagent/cli/commands.py +++ b/runagent/cli/commands.py @@ -23,7 +23,8 @@ from runagent.utils.animation import show_subtle_robotic_runner, show_quick_runner from runagent.utils.config import Config from runagent.sdk.deployment.middleware_sync import get_middleware_sync - +from runagent.cli.utils import add_framework_options, get_selected_framework +from runagent.utils.enums.framework import Framework console = Console() @click.command() @@ -229,150 +230,130 @@ def delete(agent_id, yes): raise click.ClickException("Delete failed") - - - @click.command() @click.option("--template", default="default", help="Template variant (basic, advanced, default)") @click.option("--interactive", "-i", is_flag=True, help="Enable interactive prompts") @click.option("--overwrite", is_flag=True, help="Overwrite existing folder") -@click.option("--ag2", is_flag=True, help="Use AG2 framework") -@click.option("--agno", is_flag=True, help="Use AGNO framework") -@click.option("--autogen", is_flag=True, help="Use Autogen framework") -@click.option("--crewai", is_flag=True, help="Use CrewAI framework") -@click.option("--langchain", is_flag=True, help="Use LangChain framework") -@click.option("--langgraph", is_flag=True, help="Use LangGraph framework") -@click.option("--letta", is_flag=True, help="Use Letta framework") -@click.option("--llamaindex", is_flag=True, help="Use LlamaIndex framework") -@click.option("--openai", is_flag=True, help="Use OpenAI framework") -@click.option("--n8n", is_flag=True, help="Use N8N workflows") +@add_framework_options # This automatically adds all framework options! @click.argument( "path", type=click.Path( - file_okay=False, # Don't allow files - dir_okay=True, # Allow directories only - readable=True, # Must be readable - resolve_path=True, # Convert to absolute path - path_type=Path, # Return as pathlib.Path object + file_okay=False, + dir_okay=True, + readable=True, + resolve_path=True, + path_type=Path, ), default=".", - required=False, # Make path optional + required=False, ) -def init( - template, - interactive, - overwrite, - ag2, - agno, - autogen, - crewai, - langchain, - langgraph, - letta, - llamaindex, - openai, - n8n, - path -): +def init(template, interactive, overwrite, path, **kwargs): """Initialize a new RunAgent project""" - + try: sdk = RunAgent() - - # Check for mutually exclusive framework flags - framework_dict = { - "ag2": ag2, - "agno": agno, - "autogen": autogen, - "crewai": crewai, - "langchain": langchain, - "langgraph": langgraph, - "letta": letta, - "llamaindex": llamaindex, - "openai": openai, - "n8n": n8n - } - total_flags = sum(flag for flag in framework_dict.values()) - if total_flags > 1: - frameworks_str = ", ".join(f"--{fw}" for fw in framework_dict) - raise click.UsageError(f"Only one framework can be specified: {frameworks_str}") - - framework = ( - [name for name, flag in framework_dict.items() if flag] or ["default"] - )[0] + + # Extract selected framework using our helper + selected_framework = get_selected_framework(kwargs) + framework = selected_framework if selected_framework else Framework.DEFAULT if interactive: - if framework == "default": + if framework == Framework.DEFAULT: console.print("šŸŽÆ [bold]Available frameworks:[/bold]") - for i, fw in enumerate(framework_dict.keys(), 1): # need to start from 1 - console.print(f" {i}. {fw}") - + selectable_frameworks = Framework.get_selectable_frameworks() + + for i, fw in enumerate(selectable_frameworks, 1): + category_emoji = "šŸ" if fw.is_pythonic() else "🌐" if fw.is_webhook() else "ā“" + console.print(f" {i}. {category_emoji} {fw.value} ({fw.category})") + choice = click.prompt( - "Select framework", type=click.IntRange(1, len(framework_dict)), default=1 + "Select framework", + type=click.IntRange(1, len(selectable_frameworks)), + default=1 ) - # framework = framework_dict[choice - 1] - framework = [ - fw_name for i, fw_name in enumerate(framework_dict) if i == (choice-1) - ][0] - + framework = selectable_frameworks[choice - 1] + if template == "default": - templates = sdk.list_templates(framework) - template_list = templates.get(framework, ["default"]) - - console.print(f"\n🧱 [bold]Available templates for {framework}:[/bold]") + templates = sdk.list_templates(framework.value) + template_list = templates.get(framework.value, ["default"]) + + console.print(f"\n🧱 [bold]Available templates for {framework.value}:[/bold]") for i, tmpl in enumerate(template_list, 1): console.print(f" {i}. {tmpl}") - + choice = click.prompt( - "Select template", type=click.IntRange(1, len(template_list)), default=1 + "Select template", + type=click.IntRange(1, len(template_list)), + default=1 ) template = template_list[choice - 1] - + if path.resolve() == Path.cwd(): project_name = click.prompt( "Enter project name", type=str, default="runagent-project" ) - # Update path to include project name path = Path.cwd() / project_name - + + # Validate framework if it came from string input + if isinstance(framework, str): + try: + framework = Framework.from_string(framework) + except ValueError as e: + raise click.UsageError(str(e)) + # Use the path as the project location project_path = path.resolve() relative_project_path = project_path.relative_to(Path.cwd()) - - # Ensure the path exists (create parent directories if needed) + + # Ensure the path exists project_path.parent.mkdir(parents=True, exist_ok=True) - - # Show configuration + + # Show configuration with enhanced formatting console.print(f"\nšŸš€ [bold]Initializing project:[/bold]") - console.print(f" Path: [cyan]{relative_project_path}[/cyan]") - console.print(f" Framework: [magenta]{framework if framework else 'None'}[/magenta]") + + # Enhanced framework display with category + framework_display = framework.value + if not framework.is_default(): + category_emoji = "šŸ" if framework.is_pythonic() else "🌐" if framework.is_webhook() else "ā“" + framework_display = f"{category_emoji} {framework.value} ({framework.category})" + + console.print(f" Framework: [magenta]{framework_display}[/magenta]") console.print(f" Template: [yellow]{template}[/yellow]") - - print(">>", framework, ">>", template) + # Initialize project success = sdk.init_project( folder_path=project_path, - framework=framework, + framework=framework.value, # Pass the string value template=template, overwrite=overwrite ) - + if success: console.print(f"\nāœ… [green]Project initialized successfully![/green]") console.print(f"šŸ“ Created at: [cyan]{relative_project_path}[/cyan]") - - # Show next steps + + # Enhanced next steps with framework-specific guidance console.print("\nšŸ“ [bold]Next steps:[/bold]") console.print(f" 1. [cyan]cd {relative_project_path}[/cyan]") console.print(f" 2. Update your API keys in [yellow].env[/yellow] file") - console.print(f" 3. Deploy locally: [cyan]runagent serve {relative_project_path}[/cyan]") + + # Framework-specific guidance + if framework.is_pythonic(): + console.print(f" 3. Install dependencies: [cyan]pip install -r requirements.txt[/cyan]") + console.print(f" 4. Deploy locally: [cyan]runagent serve {relative_project_path}[/cyan]") + elif framework.is_webhook(): + console.print(f" 3. Configure webhook endpoints in your workflow") + console.print(f" 4. Deploy locally: [cyan]runagent serve {relative_project_path}[/cyan]") + else: + console.print(f" 3. Deploy locally: [cyan]runagent serve {relative_project_path}[/cyan]") + console.print( - f" 4. Test: [cyan]Test the agent with any of our SDKs. For more details, refer to: [link]https://docs.run-agent.ai/sdk/overview[/link][/cyan]" + f" 5. Test: [cyan]Test the agent with any of our SDKs. For more details, refer to: [link]https://docs.run-agent.ai/sdk/overview[/link][/cyan]" ) - + except TemplateError as e: if os.getenv('DISABLE_TRY_CATCH'): raise @@ -384,12 +365,16 @@ def init( console.print(f"āŒ [red]Path exists:[/red] {e}") console.print("šŸ’” Use [cyan]--overwrite[/cyan] to force initialization") raise click.ClickException("Project initialization failed") + except click.UsageError: + # Re-raise UsageError as-is for proper click handling + raise except Exception as e: if os.getenv('DISABLE_TRY_CATCH'): raise console.print(f"āŒ [red]Initialization error:[/red] {e}") raise click.ClickException("Project initialization failed") + @click.command() @click.option( "--list", "action_list", is_flag=True, help="List all available templates" @@ -855,7 +840,7 @@ def serve(port, host, debug, replace, no_animation, animation_style, path): agent_path=str(path), host=allocated_host, port=allocated_port, # Ensure port is not None - framework=detect_framework(path), + framework=detect_framework(path).value, ) if not result["success"]: @@ -1070,6 +1055,8 @@ def run(ctx, agent_id, host, port, input_file, local, tag, timeout): # Local execution if local: console.print(" Local: [green]Yes[/green]") + else: + console.print(" Local: [red]No(Deployed to RunAgent Cloud)[/red]") # Timeout if timeout: diff --git a/runagent/sdk/deployment/remote.py b/runagent/sdk/deployment/remote.py index 8cd2b38..a6b94fd 100644 --- a/runagent/sdk/deployment/remote.py +++ b/runagent/sdk/deployment/remote.py @@ -59,7 +59,6 @@ def upload_agent( Args: folder_path: Path to agent folder - framework: Framework type (auto-detected if None) Returns: Upload result @@ -72,9 +71,8 @@ def upload_agent( # if not framework: # framework = self._detect_framework(folder_path) - metadata = {"framework": framework} - - return self.client.upload_agent(folder_path=str(folder_path), metadata=metadata) + # metadata = {"framework": framework} + return self.client.upload_agent(folder_path=str(folder_path)) def start_agent( self, agent_id: str, config: t.Optional[t.Dict[str, t.Any]] = None diff --git a/runagent/sdk/rest_client.py b/runagent/sdk/rest_client.py index 866854e..1264f28 100644 --- a/runagent/sdk/rest_client.py +++ b/runagent/sdk/rest_client.py @@ -4,10 +4,8 @@ import tempfile import time import zipfile -import threading from pathlib import Path from typing import Any, Dict, Optional, Union -from concurrent.futures import ThreadPoolExecutor, as_completed import requests from rich.console import Console @@ -377,10 +375,10 @@ def clear_limits_cache(self): self._cache_expiry = None console.print("šŸ”„ [dim]Limits cache cleared[/dim]") - def _create_zip_from_folder(self, folder_path: Path) -> str: + def _create_zip_from_folder(self, agent_id: str, folder_path: Path) -> str: """Create a zip file from the agent folder""" temp_dir = tempfile.gettempdir() - zip_path = os.path.join(temp_dir, f"agent_{int(time.time())}.zip") + zip_path = os.path.join(temp_dir, f"agent_{agent_id[:8]}.zip") with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as zipf: for file_path in folder_path.rglob("*"): @@ -396,20 +394,21 @@ def _create_zip_from_folder(self, folder_path: Path) -> str: return zip_path - def _upload_metadata(self, agent_config: Dict, agent_id: str) -> Dict: - """Upload agent metadata to middleware server using full agent config""" + def _upload_agent_metadata_to_server(self, config_data: Dict, agent_id: str) -> Dict: + """Upload agent metadata (config, entrypoints) to middleware server""" try: if not agent_id: return {"success": False, "error": "No agent_id provided"} # Use the full agent config directly - payload = { - "id": agent_id, - "config": agent_config - } + # payload = { + # "id": agent_id, + # "config": agent_config + # } try: - response = self.http.post("/agents/metadata-upload", json=payload, timeout=60) + + response = self.http.post("/agents/metadata-upload", data=config_data, timeout=60) result = response.json() # Handle new API response format @@ -434,66 +433,49 @@ def _upload_metadata(self, agent_config: Dict, agent_id: str) -> Dict: except Exception as e: return {"success": False, "error": f"Metadata upload error: {str(e)}"} - def _upload_to_server_secure(self, zip_path: str, agent_config: Dict, agent_id: str, progress: Progress, task_id) -> Dict: - """Upload zip file to middleware server with parallel processing""" + def _upload_agent_zip_file_to_server(self, zip_path: str, agent_id: str, progress: Progress, task_id) -> Dict: + """Upload agent zip file (source code) to middleware server""" try: - # Step 1: Start metadata upload asynchronously - progress.update(task_id, completed=5, description="Starting metadata upload...") + # Upload zip file + file_size = os.path.getsize(zip_path) + progress.update(task_id, completed=30, description=f"Uploading {os.path.basename(zip_path)} ({file_size:,} bytes)...") - # Use ThreadPoolExecutor for parallel processing - with ThreadPoolExecutor(max_workers=2) as executor: - # Submit metadata upload task - metadata_future = executor.submit(self._upload_metadata, agent_config, agent_id) - - # Wait for metadata upload to complete - progress.update(task_id, completed=15, description="Waiting for metadata upload...") - metadata_result = metadata_future.result() - - if not metadata_result.get("success"): - return {"success": False, "error": f"Metadata upload failed: {metadata_result.get('error')}"} - - progress.update(task_id, completed=20, description="Metadata uploaded successfully") - - # Step 2: Upload file - file_size = os.path.getsize(zip_path) - progress.update(task_id, completed=25, description=f"Uploading {os.path.basename(zip_path)} ({file_size:,} bytes)...") - - with open(zip_path, "rb") as f: - files = {"file": (os.path.basename(zip_path), f, "application/zip")} - data = { - "agent_id": agent_id, - } + with open(zip_path, "rb") as f: + files = {"file": (os.path.basename(zip_path), f, "application/zip")} + data = { + "agent_id": agent_id, + } - try: - response = self.http.post("/agents/upload", files=files, data=data, timeout=300) - - # Complete the progress bar - progress.update(task_id, completed=95, description="Processing upload...") - result = response.json() + try: + response = self.http.post("/agents/upload", files=files, data=data, timeout=300) + + # Complete the progress bar + progress.update(task_id, completed=95, description="Processing upload...") + result = response.json() + + # Handle new API response format + if result.get("success"): + progress.update(task_id, completed=100, description="Upload completed!") + time.sleep(0.5) # Brief pause to show completion - # Handle new API response format - if result.get("success"): - progress.update(task_id, completed=100, description="Upload completed!") - time.sleep(0.5) # Brief pause to show completion - - return { - "success": True, - "agent_id": result.get("data", {}).get("agent_id", agent_id), - "message": result.get("message", "Upload completed"), - "status": result.get("data", {}).get("status", "uploaded"), - "file_size": result.get("data", {}).get("file_size", file_size), - "file_name": result.get("data", {}).get("file_name", os.path.basename(zip_path)) - } - else: - error_info = result.get("error", {}) - return { - "success": False, - "error": f"File upload failed: {error_info.get('message', 'Unknown error')}", - "error_code": error_info.get("code", "UNKNOWN_ERROR") - } - - except (ClientError, ServerError, ConnectionError) as e: - return {"success": False, "error": f"File upload failed: {e.message}"} + return { + "success": True, + "agent_id": result.get("data", {}).get("agent_id", agent_id), + "message": result.get("message", "Upload completed"), + "status": result.get("data", {}).get("status", "uploaded"), + "file_size": result.get("data", {}).get("file_size", file_size), + "file_name": result.get("data", {}).get("file_name", os.path.basename(zip_path)) + } + else: + error_info = result.get("error", {}) + return { + "success": False, + "error": f"File upload failed: {error_info.get('message', 'Unknown error')}", + "error_code": error_info.get("code", "UNKNOWN_ERROR") + } + + except (ClientError, ServerError, ConnectionError) as e: + return {"success": False, "error": f"File upload failed: {e.message}"} except Exception as e: return {"success": False, "error": f"Upload error: {str(e)}"} @@ -550,8 +532,8 @@ def _process_upload_result(self, result: Dict, upload_metadata: Dict) -> Dict: } return result - def upload_agent(self, folder_path: str, metadata: Dict = None) -> Dict: - """Upload agent folder to middleware server with parallel processing and validation""" + def upload_agent(self, folder_path: str) -> Dict: + """Upload agent folder to middleware server with validation""" try: folder_path = Path(folder_path) @@ -562,6 +544,7 @@ def upload_agent(self, folder_path: str, metadata: Dict = None) -> Dict: # Step 1: Validate agent console.print(f"šŸ” Validating agent...") + is_valid, validation_details = validate_agent(folder_path) if not is_valid: @@ -681,19 +664,28 @@ def upload_agent(self, folder_path: str, metadata: Dict = None) -> Dict: ) as progress: upload_task = progress.add_task("Initializing upload...", total=100) - # Create zip file in parallel with metadata upload - with ThreadPoolExecutor(max_workers=2) as executor: - # Submit zip creation task - zip_future = executor.submit(self._create_zip_from_folder, folder_path) - - # Wait for zip creation to complete - progress.update(upload_task, completed=10, description="Creating upload package...") - zip_path = zip_future.result() - - console.print(f"šŸ“¦ Created upload package: [cyan]{Path(zip_path).name}[/cyan]") - - # Now upload with the new parallel method - result = self._upload_to_server_secure(zip_path, agent_config, agent_id, progress, upload_task) + # Step 1: Upload metadata first + progress.update(upload_task, completed=10, description="Uploading agent metadata...") + config_data = { + "id": agent_id, + "config": agent_config.to_dict() + } + + metadata_result = self._upload_agent_metadata_to_server(config_data, agent_id) + + if not metadata_result.get("success"): + return {"success": False, "error": f"Metadata upload failed: {metadata_result.get('error')}"} + + progress.update(upload_task, completed=20, description="Metadata uploaded successfully") + + # Step 2: Create zip file + progress.update(upload_task, completed=25, description="Creating upload package...") + zip_path = self._create_zip_from_folder(agent_id, folder_path) + + console.print(f"šŸ“¦ Created upload package: [cyan]{Path(zip_path).name}[/cyan]") + + # Step 3: Upload zip file + result = self._upload_agent_zip_file_to_server(zip_path, agent_id, progress, upload_task) # Clean up zip file os.unlink(zip_path) @@ -728,7 +720,8 @@ def start_agent(self, agent_id: str, config: Dict = None) -> Dict: def _process_start_result(self, result: Dict, agent_id: str) -> Dict: """Process start agent result""" if result.get("success"): - endpoint = result.get("endpoint") + result_data = result["data"] + endpoint = result_data.get("endpoint") console.print(Panel( f"āœ… [bold green]Agent started successfully![/bold green]\n" @@ -848,422 +841,422 @@ def _get_local_deployment_info(self, agent_id: str) -> Optional[Dict]: # runagent/sdk/rest_client.py - FIXED RestClient initialization -class RestClient: - """Client for remote server deployment via REST API""" - - def __init__( - self, - base_url: Optional[str] = None, - api_key: Optional[str] = None, - api_prefix: Optional[str] = "/api/v1", - ): - """Initialize REST client for middleware server""" - self.api_key = api_key or Config.get_api_key() +# class RestClient: +# """Client for remote server deployment via REST API""" + +# def __init__( +# self, +# base_url: Optional[str] = None, +# api_key: Optional[str] = None, +# api_prefix: Optional[str] = "/api/v1", +# ): +# """Initialize REST client for middleware server""" +# self.api_key = api_key or Config.get_api_key() - # Fix base URL construction - if base_url: - self.base_url = base_url.rstrip("/") + api_prefix - else: - raw_base_url = Config.get_base_url() - self.base_url = raw_base_url.rstrip("/") + api_prefix - - # Initialize HTTP handler directly with API key - # The middleware auth system will handle JWT conversion automatically - self.http = HttpHandler( - api_key=self.api_key, # Use API key directly - middleware handles conversion - base_url=self.base_url - ) - - # Cache for limits to avoid repeated API calls - self._limits_cache = None - self._cache_expiry = None - - - def validate_api_connection(self) -> Dict[str, Any]: - """Validate API connection and authentication - SIMPLIFIED""" - try: - # Test basic connectivity first - try: - health_response = self.http.get("/health", timeout=10, handle_errors=False) +# # Fix base URL construction +# if base_url: +# self.base_url = base_url.rstrip("/") + api_prefix +# else: +# raw_base_url = Config.get_base_url() +# self.base_url = raw_base_url.rstrip("/") + api_prefix + +# # Initialize HTTP handler directly with API key +# # The middleware auth system will handle JWT conversion automatically +# self.http = HttpHandler( +# api_key=self.api_key, # Use API key directly - middleware handles conversion +# base_url=self.base_url +# ) + +# # Cache for limits to avoid repeated API calls +# self._limits_cache = None +# self._cache_expiry = None + + +# def validate_api_connection(self) -> Dict[str, Any]: +# """Validate API connection and authentication - SIMPLIFIED""" +# try: +# # Test basic connectivity first +# try: +# health_response = self.http.get("/health", timeout=10, handle_errors=False) - if health_response.status_code != 200: - return { - "success": False, - "api_connected": False, - "error": f"Health check failed: {health_response.status_code}", - } - - except Exception as e: - return { - "success": False, - "api_connected": False, - "error": f"Cannot connect to middleware: {str(e)}", - } - - # Test authentication if API key provided - if self.api_key: - try: - # Try to get user profile which requires authentication - auth_response = self.http.get("/users/profile", timeout=10) +# if health_response.status_code != 200: +# return { +# "success": False, +# "api_connected": False, +# "error": f"Health check failed: {health_response.status_code}", +# } + +# except Exception as e: +# return { +# "success": False, +# "api_connected": False, +# "error": f"Cannot connect to middleware: {str(e)}", +# } + +# # Test authentication if API key provided +# if self.api_key: +# try: +# # Try to get user profile which requires authentication +# auth_response = self.http.get("/users/profile", timeout=10) - if auth_response.status_code == 200: - profile_data = auth_response.json() - auth_data = profile_data.get("auth_data", {}) +# if auth_response.status_code == 200: +# profile_data = auth_response.json() +# auth_data = profile_data.get("auth_data", {}) - return { - "success": True, - "api_connected": True, - "api_authenticated": True, - "user_info": { - "email": auth_data.get("email"), - "id": auth_data.get("id") - }, - "base_url": self.base_url, - } - else: - error_data = auth_response.json() if hasattr(auth_response, 'json') else {} - return { - "success": False, - "api_connected": True, - "api_authenticated": False, - "error": error_data.get("detail", f"Authentication failed: {auth_response.status_code}"), - "base_url": self.base_url, - } +# return { +# "success": True, +# "api_connected": True, +# "api_authenticated": True, +# "user_info": { +# "email": auth_data.get("email"), +# "id": auth_data.get("id") +# }, +# "base_url": self.base_url, +# } +# else: +# error_data = auth_response.json() if hasattr(auth_response, 'json') else {} +# return { +# "success": False, +# "api_connected": True, +# "api_authenticated": False, +# "error": error_data.get("detail", f"Authentication failed: {auth_response.status_code}"), +# "base_url": self.base_url, +# } - except AuthenticationError as e: - return { - "success": False, - "api_connected": True, - "api_authenticated": False, - "error": f"Invalid API key: {e.message}", - "base_url": self.base_url, - } - except Exception as e: - return { - "success": False, - "api_connected": True, - "api_authenticated": False, - "error": f"Authentication test failed: {str(e)}", - "base_url": self.base_url, - } - else: - return { - "success": True, - "api_connected": True, - "api_authenticated": False, - "base_url": self.base_url, - "message": "No API key provided", - } - - except Exception as e: - return { - "success": False, - "api_connected": False, - "error": f"Connection validation failed: {str(e)}", - } - - - - def _make_request(self, method: str, endpoint: str, **kwargs) -> dict: - """ - Make authenticated HTTP request to middleware API (for middleware sync) +# except AuthenticationError as e: +# return { +# "success": False, +# "api_connected": True, +# "api_authenticated": False, +# "error": f"Invalid API key: {e.message}", +# "base_url": self.base_url, +# } +# except Exception as e: +# return { +# "success": False, +# "api_connected": True, +# "api_authenticated": False, +# "error": f"Authentication test failed: {str(e)}", +# "base_url": self.base_url, +# } +# else: +# return { +# "success": True, +# "api_connected": True, +# "api_authenticated": False, +# "base_url": self.base_url, +# "message": "No API key provided", +# } + +# except Exception as e: +# return { +# "success": False, +# "api_connected": False, +# "error": f"Connection validation failed: {str(e)}", +# } + + + +# def _make_request(self, method: str, endpoint: str, **kwargs) -> dict: +# """ +# Make authenticated HTTP request to middleware API (for middleware sync) - Args: - method: HTTP method (GET, POST, PUT, DELETE) - endpoint: API endpoint path - **kwargs: Additional arguments for requests +# Args: +# method: HTTP method (GET, POST, PUT, DELETE) +# endpoint: API endpoint path +# **kwargs: Additional arguments for requests - Returns: - Response data as dict - """ - if not self.api_key: - raise Exception("API key not configured") +# Returns: +# Response data as dict +# """ +# if not self.api_key: +# raise Exception("API key not configured") - try: - # Convert 'json' kwarg to 'data' to match HttpHandler interface - if 'json' in kwargs: - kwargs['data'] = kwargs.pop('json') +# try: +# # Convert 'json' kwarg to 'data' to match HttpHandler interface +# if 'json' in kwargs: +# kwargs['data'] = kwargs.pop('json') - # Use the existing HTTP handler for consistency - if method.upper() == "GET": - response = self.http.get(endpoint, **kwargs) - elif method.upper() == "POST": - response = self.http.post(endpoint, **kwargs) - elif method.upper() == "PUT": - response = self.http.put(endpoint, **kwargs) - elif method.upper() == "DELETE": - response = self.http.delete(endpoint, **kwargs) - elif method.upper() == "PATCH": - response = self.http.patch(endpoint, **kwargs) - else: - raise ValueError(f"Unsupported HTTP method: {method}") +# # Use the existing HTTP handler for consistency +# if method.upper() == "GET": +# response = self.http.get(endpoint, **kwargs) +# elif method.upper() == "POST": +# response = self.http.post(endpoint, **kwargs) +# elif method.upper() == "PUT": +# response = self.http.put(endpoint, **kwargs) +# elif method.upper() == "DELETE": +# response = self.http.delete(endpoint, **kwargs) +# elif method.upper() == "PATCH": +# response = self.http.patch(endpoint, **kwargs) +# else: +# raise ValueError(f"Unsupported HTTP method: {method}") - # Handle response - check if it's already a dict or a Response object - if hasattr(response, 'json') and callable(response.json): - return response.json() - elif isinstance(response, dict): - return response - else: - # Fallback: try to get json content - try: - return response.json() - except: - return {"success": False, "error": "Invalid response format"} +# # Handle response - check if it's already a dict or a Response object +# if hasattr(response, 'json') and callable(response.json): +# return response.json() +# elif isinstance(response, dict): +# return response +# else: +# # Fallback: try to get json content +# try: +# return response.json() +# except: +# return {"success": False, "error": "Invalid response format"} - except (ClientError, ServerError, ConnectionError, AuthenticationError, ValidationError) as e: - raise Exception(f"API request failed: {e.message}") - except Exception as e: - raise Exception(f"Request error: {e}") - - def run_agent( - self, - agent_id: str, - entrypoint_tag: str, - input_args: list = None, - input_kwargs: dict = None, - execution_type: str = "generic", - ) -> Dict: - """Execute an agent with given parameters""" - try: - console.print(f"šŸ¤– Executing agent: [bold magenta]{agent_id}[/bold magenta]") - - # Prepare request data - request_data = { - "input_data": {"input_args": input_args, "input_kwargs": input_kwargs} - } - - # Execute the agent - try: - response = self.http.post( - f"/agents/{agent_id}/execute/{entrypoint_tag}", - data=request_data, - timeout=120, # Longer timeout for agent execution - ) - result = response.json() - - if result.get("success", True): # Assume success if not explicitly false - console.print("āœ… [bold green]Agent execution completed![/bold green]") - return result - else: - console.print(f"āŒ [bold red]Agent execution failed: {result.get('error', 'Unknown error')}[/bold red]") - return result - - except (ClientError, ServerError, ConnectionError) as e: - return {"success": False, "error": f"Agent execution failed: {e.message}"} - - except Exception as e: - return {"success": False, "error": f"Execute agent failed: {str(e)}"} - - def get_agent_architecture(self, agent_id: str) -> Dict: - """Get the architecture information for a specific agent""" - try: - response = self.http.get(f"/agents/{agent_id}/architecture") - return response.json() - except Exception as e: - return {"success": False, "error": f"Failed to get architecture: {str(e)}"} - - - def sync_local_agent(self, agent_data: Dict[str, Any]) -> Dict[str, Any]: - """Sync local agent to middleware""" - try: - response = self.http.post("/local-agents", data=agent_data, timeout=30) - return response.json() - except Exception as e: - return {"success": False, "error": str(e)} - - def create_local_invocation(self, invocation_data: Dict[str, Any]) -> Dict[str, Any]: - """Create local invocation in middleware""" - try: - response = self.http.post("/local-invocations", data=invocation_data, timeout=30) - return response.json() - except Exception as e: - return {"success": False, "error": str(e)} - - def update_local_invocation(self, invocation_id: str, update_data: Dict[str, Any]) -> Dict[str, Any]: - """Update local invocation in middleware""" - try: - response = self.http.put(f"/local-invocations/{invocation_id}", data=update_data, timeout=30) - return response.json() - except Exception as e: - return {"success": False, "error": str(e)} - - def _get_jwt_token_from_api_key(self) -> Optional[str]: - """Convert API key to JWT token using middleware auth endpoint""" - if not self.api_key: - return None +# except (ClientError, ServerError, ConnectionError, AuthenticationError, ValidationError) as e: +# raise Exception(f"API request failed: {e.message}") +# except Exception as e: +# raise Exception(f"Request error: {e}") + +# def run_agent( +# self, +# agent_id: str, +# entrypoint_tag: str, +# input_args: list = None, +# input_kwargs: dict = None, +# execution_type: str = "generic", +# ) -> Dict: +# """Execute an agent with given parameters""" +# try: +# console.print(f"šŸ¤– Executing agent: [bold magenta]{agent_id}[/bold magenta]") + +# # Prepare request data +# request_data = { +# "input_data": {"input_args": input_args, "input_kwargs": input_kwargs} +# } + +# # Execute the agent +# try: +# response = self.http.post( +# f"/agents/{agent_id}/execute/{entrypoint_tag}", +# data=request_data, +# timeout=120, # Longer timeout for agent execution +# ) +# result = response.json() + +# if result.get("success", True): # Assume success if not explicitly false +# console.print("āœ… [bold green]Agent execution completed![/bold green]") +# return result +# else: +# console.print(f"āŒ [bold red]Agent execution failed: {result.get('error', 'Unknown error')}[/bold red]") +# return result + +# except (ClientError, ServerError, ConnectionError) as e: +# return {"success": False, "error": f"Agent execution failed: {e.message}"} + +# except Exception as e: +# return {"success": False, "error": f"Execute agent failed: {str(e)}"} + +# def get_agent_architecture(self, agent_id: str) -> Dict: +# """Get the architecture information for a specific agent""" +# try: +# response = self.http.get(f"/agents/{agent_id}/architecture") +# return response.json() +# except Exception as e: +# return {"success": False, "error": f"Failed to get architecture: {str(e)}"} + + +# def sync_local_agent(self, agent_data: Dict[str, Any]) -> Dict[str, Any]: +# """Sync local agent to middleware""" +# try: +# response = self.http.post("/local-agents", data=agent_data, timeout=30) +# return response.json() +# except Exception as e: +# return {"success": False, "error": str(e)} + +# def create_local_invocation(self, invocation_data: Dict[str, Any]) -> Dict[str, Any]: +# """Create local invocation in middleware""" +# try: +# response = self.http.post("/local-invocations", data=invocation_data, timeout=30) +# return response.json() +# except Exception as e: +# return {"success": False, "error": str(e)} + +# def update_local_invocation(self, invocation_id: str, update_data: Dict[str, Any]) -> Dict[str, Any]: +# """Update local invocation in middleware""" +# try: +# response = self.http.put(f"/local-invocations/{invocation_id}", data=update_data, timeout=30) +# return response.json() +# except Exception as e: +# return {"success": False, "error": str(e)} + +# def _get_jwt_token_from_api_key(self) -> Optional[str]: +# """Convert API key to JWT token using middleware auth endpoint""" +# if not self.api_key: +# return None - try: - # Check if API key is already a JWT token - if self.api_key.startswith('eyJ'): # JWT tokens start with 'eyJ' - return self.api_key +# try: +# # Check if API key is already a JWT token +# if self.api_key.startswith('eyJ'): # JWT tokens start with 'eyJ' +# return self.api_key - # Create a temporary HTTP handler without auth for this request - temp_http = HttpHandler(base_url=self.base_url) +# # Create a temporary HTTP handler without auth for this request +# temp_http = HttpHandler(base_url=self.base_url) - # The middleware auth.py shows that API keys are validated and converted to JWT - # We'll use the validation endpoint which should return a JWT - try: - response = temp_http.post("/tokens/validate", data={"token": self.api_key}, timeout=10) +# # The middleware auth.py shows that API keys are validated and converted to JWT +# # We'll use the validation endpoint which should return a JWT +# try: +# response = temp_http.post("/tokens/validate", data={"token": self.api_key}, timeout=10) - if response.status_code == 200: - validation_result = response.json() - if validation_result.get("valid"): - - return self.api_key - else: - console.print(f"[red]API key validation failed: {response.status_code}[/red]") - return None +# if response.status_code == 200: +# validation_result = response.json() +# if validation_result.get("valid"): + +# return self.api_key +# else: +# console.print(f"[red]API key validation failed: {response.status_code}[/red]") +# return None - except Exception as e: - console.print(f"[yellow]Could not validate API key: {e}[/yellow]") - # Return API key anyway - let middleware handle the conversion - return self.api_key +# except Exception as e: +# console.print(f"[yellow]Could not validate API key: {e}[/yellow]") +# # Return API key anyway - let middleware handle the conversion +# return self.api_key - except Exception as e: - console.print(f"[red]Error processing API key: {e}[/red]") - return None - - def validate_api_connection(self) -> Dict[str, Any]: - """Validate API connection and authentication - UPDATED""" - try: - # Test basic connectivity first - try: - health_response = self.http.get("/health", timeout=10, handle_errors=False) +# except Exception as e: +# console.print(f"[red]Error processing API key: {e}[/red]") +# return None + +# def validate_api_connection(self) -> Dict[str, Any]: +# """Validate API connection and authentication - UPDATED""" +# try: +# # Test basic connectivity first +# try: +# health_response = self.http.get("/health", timeout=10, handle_errors=False) - if health_response.status_code != 200: - return { - "success": False, - "api_connected": False, - "error": f"Health check failed: {health_response.status_code}", - } - - except Exception as e: - return { - "success": False, - "api_connected": False, - "error": f"Cannot connect to middleware: {str(e)}", - } - - # Test authentication if API key provided - if self.api_key: - try: - # Try to get user profile which requires authentication - auth_response = self.http.get("/users/profile", timeout=10) +# if health_response.status_code != 200: +# return { +# "success": False, +# "api_connected": False, +# "error": f"Health check failed: {health_response.status_code}", +# } + +# except Exception as e: +# return { +# "success": False, +# "api_connected": False, +# "error": f"Cannot connect to middleware: {str(e)}", +# } + +# # Test authentication if API key provided +# if self.api_key: +# try: +# # Try to get user profile which requires authentication +# auth_response = self.http.get("/users/profile", timeout=10) - if auth_response.status_code == 200: - profile_data = auth_response.json() - user_info = profile_data.get("auth_data", {}) +# if auth_response.status_code == 200: +# profile_data = auth_response.json() +# user_info = profile_data.get("auth_data", {}) - return { - "success": True, - "api_connected": True, - "api_authenticated": True, - "user_info": { - "email": user_info.get("email"), - "id": user_info.get("id") - }, - "base_url": self.base_url, - } - else: - error_data = auth_response.json() if hasattr(auth_response, 'json') else {} - return { - "success": False, - "api_connected": True, - "api_authenticated": False, - "error": error_data.get("detail", f"Authentication failed: {auth_response.status_code}"), - "base_url": self.base_url, - } +# return { +# "success": True, +# "api_connected": True, +# "api_authenticated": True, +# "user_info": { +# "email": user_info.get("email"), +# "id": user_info.get("id") +# }, +# "base_url": self.base_url, +# } +# else: +# error_data = auth_response.json() if hasattr(auth_response, 'json') else {} +# return { +# "success": False, +# "api_connected": True, +# "api_authenticated": False, +# "error": error_data.get("detail", f"Authentication failed: {auth_response.status_code}"), +# "base_url": self.base_url, +# } - except AuthenticationError as e: - return { - "success": False, - "api_connected": True, - "api_authenticated": False, - "error": f"Invalid API key: {e.message}", - "base_url": self.base_url, - } - except Exception as e: - return { - "success": False, - "api_connected": True, - "api_authenticated": False, - "error": f"Authentication test failed: {str(e)}", - "base_url": self.base_url, - } - else: - return { - "success": True, - "api_connected": True, - "api_authenticated": False, - "base_url": self.base_url, - "message": "No API key provided", - } - - except Exception as e: - return { - "success": False, - "api_connected": False, - "error": f"Connection validation failed: {str(e)}", - } - def __del__(self): - """Cleanup on deletion""" - try: - self.close() - except: - pass - - def debug_connection(self) -> Dict: - """Debug middleware connection""" - try: - print(f"šŸ” Debug: Testing connection to {self.base_url}") +# except AuthenticationError as e: +# return { +# "success": False, +# "api_connected": True, +# "api_authenticated": False, +# "error": f"Invalid API key: {e.message}", +# "base_url": self.base_url, +# } +# except Exception as e: +# return { +# "success": False, +# "api_connected": True, +# "api_authenticated": False, +# "error": f"Authentication test failed: {str(e)}", +# "base_url": self.base_url, +# } +# else: +# return { +# "success": True, +# "api_connected": True, +# "api_authenticated": False, +# "base_url": self.base_url, +# "message": "No API key provided", +# } + +# except Exception as e: +# return { +# "success": False, +# "api_connected": False, +# "error": f"Connection validation failed: {str(e)}", +# } +# def __del__(self): +# """Cleanup on deletion""" +# try: +# self.close() +# except: +# pass + +# def debug_connection(self) -> Dict: +# """Debug middleware connection""" +# try: +# print(f"šŸ” Debug: Testing connection to {self.base_url}") - # Test health endpoint - try: - response = self.http.get("health", timeout=5) - print(f"āœ… Health check successful: {response}") - health_data = response.json() if hasattr(response, 'json') else response +# # Test health endpoint +# try: +# response = self.http.get("health", timeout=5) +# print(f"āœ… Health check successful: {response}") +# health_data = response.json() if hasattr(response, 'json') else response - # Test auth validation if API key exists - if self.api_key: - try: - auth_response = self.http.get("validate", timeout=5) - print(f"āœ… Auth validation successful: {auth_response}") - auth_data = auth_response.json() if hasattr(auth_response, 'json') else auth_response +# # Test auth validation if API key exists +# if self.api_key: +# try: +# auth_response = self.http.get("validate", timeout=5) +# print(f"āœ… Auth validation successful: {auth_response}") +# auth_data = auth_response.json() if hasattr(auth_response, 'json') else auth_response - return { - "success": True, - "health": health_data, - "auth": auth_data, - "base_url": self.base_url - } - except Exception as auth_e: - print(f"āŒ Auth validation failed: {auth_e}") - return { - "success": False, - "health": health_data, - "auth_error": str(auth_e), - "base_url": self.base_url - } - else: - return { - "success": True, - "health": health_data, - "auth": "No API key provided", - "base_url": self.base_url - } +# return { +# "success": True, +# "health": health_data, +# "auth": auth_data, +# "base_url": self.base_url +# } +# except Exception as auth_e: +# print(f"āŒ Auth validation failed: {auth_e}") +# return { +# "success": False, +# "health": health_data, +# "auth_error": str(auth_e), +# "base_url": self.base_url +# } +# else: +# return { +# "success": True, +# "health": health_data, +# "auth": "No API key provided", +# "base_url": self.base_url +# } - except Exception as health_e: - print(f"āŒ Health check failed: {health_e}") - return { - "success": False, - "health_error": str(health_e), - "base_url": self.base_url - } +# except Exception as health_e: +# print(f"āŒ Health check failed: {health_e}") +# return { +# "success": False, +# "health_error": str(health_e), +# "base_url": self.base_url +# } - except Exception as e: - print(f"āŒ Connection debug failed: {e}") - return { - "success": False, - "error": str(e), - "base_url": self.base_url - } \ No newline at end of file +# except Exception as e: +# print(f"āŒ Connection debug failed: {e}") +# return { +# "success": False, +# "error": str(e), +# "base_url": self.base_url +# } \ No newline at end of file diff --git a/runagent/sdk/sdk.py b/runagent/sdk/sdk.py index 2c70898..24652b3 100644 --- a/runagent/sdk/sdk.py +++ b/runagent/sdk/sdk.py @@ -397,18 +397,6 @@ def _get_error_suggestions(self, error_msg: str) -> t.List[str]: return suggestions - def detect_framework(self, folder: str) -> str: - """ - Auto-detect the framework used in an agent project. - - Args: - folder: Path to agent folder - - Returns: - Detected framework name - """ - return detect_framework(folder) - # Database and Server Management def cleanup_local_database(self, days_old: int = 30) -> t.Dict[str, t.Any]: """ diff --git a/runagent/sdk/server/framework/__init__.py b/runagent/sdk/server/framework/__init__.py index b909474..f0439e7 100644 --- a/runagent/sdk/server/framework/__init__.py +++ b/runagent/sdk/server/framework/__init__.py @@ -1,6 +1,7 @@ from pathlib import Path # from typing import Dict import typing as t +from runagent.utils.enums.framework import Framework from runagent.sdk.server.framework.langgraph import LangGraphExecutor from runagent.sdk.server.framework.langchain import LangChainExecutor from runagent.sdk.server.framework.openai import OpenAIExecutor @@ -15,20 +16,20 @@ def get_executor( - agent_dir: Path, framework: str, agent_entrypoints: t.Dict[str, t.Union[PythonicEntryPoint, WebHookEntryPoint]] + agent_dir: Path, framework: Framework, agent_entrypoints: t.Dict[str, t.Union[PythonicEntryPoint, WebHookEntryPoint]] ): executor_dict = { - "default": GenericExecutor, - "openai": OpenAIExecutor, - "ag2": AG2Executor, - "agno": AgnoExecutor, - "autogen": AutogenExecutor, - "crewai": CrewAIExecutor, - "langgraph": LangGraphExecutor, - "langchain": GenericExecutor, - "letta": GenericExecutor, - "llamaindex": LlamaIndexExecutor, - "n8n": N8NExecutor + Framework.DEFAULT: GenericExecutor, + Framework.OPENAI: OpenAIExecutor, + Framework.AG2: AG2Executor, + Framework.AGNO: AgnoExecutor, + Framework.AUTOGEN: AutogenExecutor, + Framework.CREWAI: CrewAIExecutor, + Framework.LANGGRAPH: LangGraphExecutor, + Framework.LANGCHAIN: GenericExecutor, + Framework.LETTA: GenericExecutor, + Framework.LLAMAINDEX: LlamaIndexExecutor, + Framework.N8N: N8NExecutor } framework_executor = executor_dict.get(framework) if framework_executor is None: diff --git a/runagent/sdk/server/local_server.py b/runagent/sdk/server/local_server.py index 446546e..7de9025 100644 --- a/runagent/sdk/server/local_server.py +++ b/runagent/sdk/server/local_server.py @@ -68,7 +68,7 @@ def __init__( self.agent_name = self.agent_config.agent_name self.agent_version = self.agent_config.version - self.agent_framework = self.agent_config.framework.value + self.agent_framework = self.agent_config.framework self.agent_architecture = self.agent_config.agent_architecture # Install dependencies if requirements.txt exists @@ -110,7 +110,7 @@ async def _sync_agent_to_middleware_and_wait(self): agent_data = { "local_agent_id": self.agent_id, # This becomes the main agent ID in middleware "name": self.agent_name, - "framework": self.agent_framework, + "framework": self.agent_framework.value, "version": self.agent_version, "path": str(self.agent_path), "host": self.host, @@ -159,7 +159,7 @@ async def run_agent(request: AgentRunRequest): "server_host": self.host, "server_port": self.port, "agent_name": self.agent_name, - "agent_framework": self.agent_framework + "agent_framework": self.agent_framework.value } ) @@ -186,7 +186,7 @@ async def run_agent(request: AgentRunRequest): "server_host": self.host, "server_port": self.port, "agent_name": self.agent_name, - "agent_framework": self.agent_framework + "agent_framework": self.agent_framework.value } } @@ -346,7 +346,7 @@ def get_server_info(self) -> dict: "server_type": "FastAPI", "agent_id": self.agent_id, "agent_name": self.agent_name, - "agent_framework": self.agent_framework, + "agent_framework": self.agent_framework.value, "invocation_tracking": True, "middleware_sync": { "enabled": sync_status["sync_enabled"], diff --git a/runagent/utils/agent.py b/runagent/utils/agent.py index b7113e3..253f9a2 100644 --- a/runagent/utils/agent.py +++ b/runagent/utils/agent.py @@ -4,7 +4,7 @@ from runagent.constants import AGENT_CONFIG_FILE_NAME from runagent.utils.imports import PackageImporter from runagent.utils.schema import RunAgentConfig -from .enums import FrameworkType +from runagent.utils.enums.framework import Framework def get_agent_config(folder_path: Path) -> t.Optional[dict]: @@ -93,7 +93,7 @@ def get_agent_config(folder_path: Path) -> t.Optional[dict]: return RunAgentConfig(**config_data) -def detect_framework(folder_path: Path) -> str: +def detect_framework(folder_path: Path) -> Framework: """ Detect framework from agent's runagent.config.json file. @@ -106,7 +106,7 @@ def detect_framework(folder_path: Path) -> str: config = get_agent_config(folder_path) framework = config.framework - return framework.value + return framework def validate_agent( @@ -129,9 +129,25 @@ def validate_agent( } config = get_agent_config(folder_path) + # framework = config.framework + if config.framework.is_pythonic(): + is_valid, details = validate_pythonic_agent(config, dynamic_loading, folder_path) + else: + is_valid, details = validate_webhook_agent(config, dynamic_loading, folder_path) + + return is_valid, details - is_valid, details = validate_pythonic_agent(config, dynamic_loading, folder_path) +def validate_webhook_agent(config, dynamic_loading, folder_path): + return True, dict( + valid=True, + folder_exists=True, + files_found=[], + missing_files=[], + success_msgs=[], + error_msgs=[], + warning_msgs=[], + ) def validate_pythonic_agent(config, dynamic_loading, folder_path): @@ -218,7 +234,7 @@ def validate_pythonic_agent(config, dynamic_loading, folder_path): # Detect framework try: - validation_details["framework"] = detect_framework(folder_path) + validation_details["framework"] = detect_framework(folder_path).value except Exception as e: validation_details["error_msgs"].append(f"Failed to detect framework: {str(e)}") validation_details["valid"] = False diff --git a/runagent/utils/enums.py b/runagent/utils/enums.py deleted file mode 100644 index 956ed27..0000000 --- a/runagent/utils/enums.py +++ /dev/null @@ -1,26 +0,0 @@ -from enum import Enum -import typing as t - - -class ResponseStatus(Enum): - SUCCESS = "success" - ERROR = "error" - - -class PythonicType(Enum): - AG2 = "ag2" - AGNO = "agno" - AUTOGEN = "autogen" - CREWAI = "crewai" - LANGCHAIN = "langchain" - LANGGRAPH = "langgraph" - LETTA = "letta" - LLAMAINDEX = "llamaindex" - OPENAI = "openai" - - -class WebhookType(Enum): - N8N = "n8n" - - -FrameworkType = t.Union[PythonicType, WebhookType] diff --git a/runagent/utils/schema.py b/runagent/utils/schema.py index f71722b..6ea0efe 100644 --- a/runagent/utils/schema.py +++ b/runagent/utils/schema.py @@ -4,7 +4,7 @@ # from typing import Any, Dict, List, Optional, Union from enum import Enum from pydantic import BaseModel, Field, validator -from .enums import FrameworkType +from runagent.utils.enums.framework import Framework class TemplateSource(BaseModel): @@ -57,13 +57,21 @@ def validate_unique_tags(cls, v): raise ValueError("All entrypoint tags must be unique") return v +from pydantic import ConfigDict + class RunAgentConfig(BaseModel): """Schema for runagent.config.json""" + # model_config = ConfigDict( + # use_enum_values=True, # Automatically convert enums to values + # json_encoders={ + # datetime: lambda v: v.isoformat() # Custom datetime serialization + # } + # ) agent_name: str = Field(..., description="Name of the agent") description: str = Field(..., description="Description of the agent") - framework: FrameworkType = Field(..., description="Framework used (langchain, etc)") + framework: Framework = Field(..., description="Framework used (langchain, etc)") template: str = Field(..., description="Template name") version: str = Field(..., description="Agent version") created_at: datetime = Field(..., description="Creation timestamp") @@ -77,6 +85,19 @@ class RunAgentConfig(BaseModel): default_factory=dict, description="Environment variables" ) + def to_dict(self) -> dict: + """Convert to dictionary with custom serialization""" + data = self.model_dump() + + # Convert enum to string value + if isinstance(data.get('framework'), Framework): + data['framework'] = data['framework'].value + + # Convert datetime to ISO string if needed + if isinstance(data.get('created_at'), datetime): + data['created_at'] = data['created_at'].isoformat() + + return data class AgentInputArgs(BaseModel): """Request model for agent execution""" From dce82a8d36f49be0ea524e6f3c66734e9f0b2f06 Mon Sep 17 00:00:00 2001 From: sawradip Date: Fri, 26 Sep 2025 02:52:21 +0600 Subject: [PATCH 2/4] fix: missing files form last version mess added --- runagent/cli/utils.py | 35 +++++ runagent/sdk/rest_client.py | 245 ++++++++++++++++++------------ runagent/utils/agent_id.py | 184 ++++++++++++++++++++++ runagent/utils/enums/framework.py | 156 +++++++++++++++++++ 4 files changed, 519 insertions(+), 101 deletions(-) create mode 100644 runagent/cli/utils.py create mode 100644 runagent/utils/agent_id.py create mode 100644 runagent/utils/enums/framework.py diff --git a/runagent/cli/utils.py b/runagent/cli/utils.py new file mode 100644 index 0000000..82e85ae --- /dev/null +++ b/runagent/cli/utils.py @@ -0,0 +1,35 @@ +import click +import typing as t +from runagent.utils.enums.framework import Framework + +# Auto-generate Click options from enum +def add_framework_options(func): + """Decorator to automatically add framework options from enum""" + selectable_frameworks = Framework.get_selectable_frameworks() + + # Add options in reverse order (Click processes them backwards) + for framework in reversed(selectable_frameworks): + option_name = f"--{framework.value}" + help_text = f"Use {framework.value.upper()} framework" + func = click.option(option_name, is_flag=True, help=help_text)(func) + + return func + + +# Helper function to extract selected framework from kwargs +def get_selected_framework(kwargs: dict) -> t.Optional[Framework]: + """Extract the selected framework from click kwargs""" + selectable_frameworks = Framework.get_selectable_frameworks() + + selected_frameworks = [ + framework for framework in selectable_frameworks + if kwargs.get(framework.value, False) + ] + + if len(selected_frameworks) > 1: + framework_names = [f"--{fw.value}" for fw in selected_frameworks] + raise click.UsageError( + f"Only one framework can be specified. Found: {', '.join(framework_names)}" + ) + + return selected_frameworks[0] if selected_frameworks else None \ No newline at end of file diff --git a/runagent/sdk/rest_client.py b/runagent/sdk/rest_client.py index 9744a88..1264f28 100644 --- a/runagent/sdk/rest_client.py +++ b/runagent/sdk/rest_client.py @@ -16,9 +16,16 @@ SpinnerColumn, TextColumn, TimeElapsedColumn, + TimeRemainingColumn, ) from runagent.utils.config import Config +from runagent.utils.agent_id import ( + generate_agent_id, + generate_agent_fingerprint, + get_agent_metadata +) +from runagent.utils.agent import get_agent_config, validate_agent console = Console() @@ -387,30 +394,6 @@ def _create_zip_from_folder(self, agent_id: str, folder_path: Path) -> str: return zip_path -<<<<<<< HEAD - def _upload_metadata(self, metadata: Dict) -> Dict: - """Upload sensitive metadata securely""" - try: - # Enhanced metadata encoding with timestamp - metadata_with_timestamp = { - **metadata, - "upload_timestamp": time.time(), - "client_version": "1.0", - } - - # Base64 encode metadata as JSON string - metadata_json = json.dumps(metadata_with_timestamp, sort_keys=True) - encrypted_data = base64.b64encode(metadata_json.encode()).decode() - - payload = { - "encrypted_metadata": encrypted_data, - "encryption_method": "base64-json", - "client_info": {"version": "1.0", "platform": os.name}, - } - - try: - response = self.http.post("/agents/metadata-upload", data=payload, timeout=60) -======= def _upload_agent_metadata_to_server(self, config_data: Dict, agent_id: str) -> Dict: """Upload agent metadata (config, entrypoints) to middleware server""" try: @@ -426,9 +409,23 @@ def _upload_agent_metadata_to_server(self, config_data: Dict, agent_id: str) -> try: response = self.http.post("/agents/metadata-upload", data=config_data, timeout=60) ->>>>>>> sawra/runagent_cloud_support result = response.json() - return {"success": True, "agent_id": result.get("agent_id")} + + # Handle new API response format + if result.get("success"): + return { + "success": True, + "agent_id": result.get("data", {}).get("agent_id", agent_id), + "entrypoints_created": result.get("data", {}).get("entrypoints_created", 0), + "entrypoint_ids": result.get("data", {}).get("entrypoint_ids", []) + } + else: + error_info = result.get("error", {}) + return { + "success": False, + "error": f"Metadata upload failed: {error_info.get('message', 'Unknown error')}", + "error_code": error_info.get("code", "UNKNOWN_ERROR") + } except (ClientError, ServerError, ConnectionError) as e: return {"success": False, "error": f"Metadata upload failed: {e.message}"} @@ -436,51 +433,6 @@ def _upload_agent_metadata_to_server(self, config_data: Dict, agent_id: str) -> except Exception as e: return {"success": False, "error": f"Metadata upload error: {str(e)}"} -<<<<<<< HEAD - def _upload_to_server_secure(self, zip_path: str, metadata: Dict, progress: Progress, task_id) -> Dict: - """Upload zip file to middleware server""" - try: - # Step 1: Upload metadata - progress.update(task_id, completed=5) - metadata_result = self._upload_metadata(metadata) - - if not metadata_result.get("success"): - return {"success": False, "error": f"Metadata upload failed: {metadata_result.get('error')}"} - - agent_id = metadata_result.get("agent_id") - progress.update(task_id, completed=20) - - # Step 2: Upload file - with open(zip_path, "rb") as f: - files = {"file": (os.path.basename(zip_path), f, "application/zip")} - data = { - "framework": metadata.get("framework", "unknown"), - "name": metadata.get("name", os.path.basename(zip_path).replace(".zip", "")), - "has_metadata": "true", - "agent_id": agent_id, - } - - # Update progress during upload - for i in range(20, 50, 5): - progress.update(task_id, completed=i) - time.sleep(0.05) - - try: - response = self.http.post("/agents/upload", files=files, data=data, timeout=300) - result = response.json() - - # Update progress during upload - for i in range(50, 100, 10): - progress.update(task_id, completed=i) - time.sleep(0.1) - - return { - "success": result.get("success", False), - "agent_id": result.get("agent_id"), - "message": result.get("message", "Upload completed"), - "status": result.get("status", "uploaded"), - } -======= def _upload_agent_zip_file_to_server(self, zip_path: str, agent_id: str, progress: Progress, task_id) -> Dict: """Upload agent zip file (source code) to middleware server""" try: @@ -521,7 +473,6 @@ def _upload_agent_zip_file_to_server(self, zip_path: str, agent_id: str, progres "error": f"File upload failed: {error_info.get('message', 'Unknown error')}", "error_code": error_info.get("code", "UNKNOWN_ERROR") } ->>>>>>> sawra/runagent_cloud_support except (ClientError, ServerError, ConnectionError) as e: return {"success": False, "error": f"File upload failed: {e.message}"} @@ -534,6 +485,28 @@ def _process_upload_result(self, result: Dict, upload_metadata: Dict) -> Dict: if result.get("success"): agent_id = result.get("agent_id") + # Save to database + try: + from runagent.sdk.db import DBService + db_service = DBService() + + # Add remote agent to database + db_result = db_service.add_remote_agent( + agent_id=agent_id, + agent_path="", # Remote agent, no local path + framework="unknown", # Will be updated when agent is started + fingerprint=upload_metadata.get("fingerprint", ""), + status="uploaded" + ) + + if db_result.get("success"): + console.print(f"šŸ’¾ [green]Agent saved to local database[/green]") + else: + console.print(f"āš ļø [yellow]Warning: Could not save to local database: {db_result.get('error')}[/yellow]") + + except Exception as e: + console.print(f"āš ļø [yellow]Warning: Database error: {str(e)}[/yellow]") + # Save deployment info locally self._save_deployment_info(agent_id, { **upload_metadata, @@ -545,7 +518,8 @@ def _process_upload_result(self, result: Dict, upload_metadata: Dict) -> Dict: console.print(Panel( f"āœ… [bold green]Upload successful![/bold green]\n" f"šŸ†” Agent ID: [bold magenta]{agent_id}[/bold magenta]\n" - f"🌐 Server: [blue]{self.base_url}[/blue]", + f"🌐 Server: [blue]{self.base_url}[/blue]\n" + f"šŸ” Fingerprint: [dim]{upload_metadata.get('fingerprint', 'N/A')[:16]}...[/dim]", title="šŸ“¤ Upload Complete", border_style="green", )) @@ -558,13 +532,8 @@ def _process_upload_result(self, result: Dict, upload_metadata: Dict) -> Dict: } return result -<<<<<<< HEAD - def upload_agent(self, folder_path: str, metadata: Dict = None) -> Dict: - """Upload agent folder to middleware server""" -======= def upload_agent(self, folder_path: str) -> Dict: """Upload agent folder to middleware server with validation""" ->>>>>>> sawra/runagent_cloud_support try: folder_path = Path(folder_path) @@ -573,11 +542,6 @@ def upload_agent(self, folder_path: str) -> Dict: console.print(f"šŸ“¤ Uploading agent from: [blue]{folder_path}[/blue]") -<<<<<<< HEAD - # Create zip file - with console.status("[bold green]šŸ”§ Preparing files for upload...[/bold green]", spinner="dots"): - zip_path = self._create_zip_from_folder(folder_path) -======= # Step 1: Validate agent console.print(f"šŸ” Validating agent...") @@ -595,33 +559,109 @@ def upload_agent(self, folder_path: str) -> Dict: } console.print(f"āœ… [green]Agent validation passed[/green]") ->>>>>>> sawra/runagent_cloud_support - console.print(f"šŸ“¦ Created upload package: [cyan]{Path(zip_path).name}[/cyan]") - - # Prepare upload metadata - upload_metadata = { - "framework": (metadata.get("framework", "unknown") if metadata else "unknown"), - "uploaded_at": time.strftime("%Y-%m-%d %H:%M:%S"), - "source_folder": str(folder_path), - **(metadata or {}), - } + # Step 2: Load agent config + try: + agent_config = get_agent_config(folder_path) + console.print(f"šŸ“‹ [green]Agent config loaded successfully[/green]") + except Exception as e: + return {"success": False, "error": f"Failed to load agent config: {str(e)}"} + + # Step 3: Generate agent fingerprint for duplicate detection + fingerprint = generate_agent_fingerprint(folder_path) + console.print(f"šŸ” Agent fingerprint: [dim]{fingerprint[:16]}...[/dim]") + + # Step 4: Check for existing agents (both by fingerprint and by path) + from runagent.sdk.db import DBService + db_service = DBService() + + # Check for exact fingerprint match (identical content) + existing_agent_by_fingerprint = db_service.get_agent_by_fingerprint(fingerprint) + + # Check for existing agent by path (same folder, potentially modified) + existing_agent_by_path = db_service.get_agent_by_path(str(folder_path)) + + if existing_agent_by_fingerprint: + # Identical content detected + existing_agent = existing_agent_by_fingerprint + console.print(f"āš ļø [yellow]Agent with identical content already exists![/yellow]") + console.print(f"šŸ†” Existing Agent ID: [magenta]{existing_agent['agent_id']}[/magenta]") + console.print(f"šŸ“Š Status: [cyan]{existing_agent['status']}[/cyan]") + console.print(f"šŸ“ Type: [cyan]{'Local' if existing_agent['is_local'] else 'Remote'}[/cyan]") + + # Ask user if they want to overwrite identical content + from rich.prompt import Confirm + overwrite = Confirm.ask("Do you want to overwrite the existing agent?", default=False) + + if not overwrite: + return { + "success": False, + "error": "Upload cancelled by user", + "code": "USER_CANCELLED", + "existing_agent": existing_agent + } + + # Use existing agent ID for overwrite + agent_id = existing_agent['agent_id'] + console.print(f"šŸ”„ [yellow]Overwriting existing agent: {agent_id}[/yellow]") + + elif existing_agent_by_path: + # Modified content detected (same folder, different fingerprint) + existing_agent = existing_agent_by_path + console.print(f"āš ļø [yellow]Agent content has changed![/yellow]") + console.print(f"šŸ†” Existing Agent ID: [magenta]{existing_agent['agent_id']}[/magenta]") + console.print(f"šŸ“Š Status: [cyan]{existing_agent['status']}[/cyan]") + console.print(f"šŸ“ Type: [cyan]{'Local' if existing_agent['is_local'] else 'Remote'}[/cyan]") + console.print(f"šŸ” Content fingerprint changed (modified files detected)") + + # Show enhanced options for modified content + from rich.prompt import Prompt + choice = Prompt.ask( + "What would you like to do?", + choices=["overwrite", "new", "cancel"], + default="new" + ) + + if choice == "overwrite": + # Feature not available yet - show message and fallback + console.print(f"\n🚧 [yellow]Overwrite functionality is not yet available.[/yellow]") + console.print(f"šŸ’” [cyan]This feature is coming soon! For now, we'll create a new agent.[/cyan]") + console.print(f"šŸ“¢ [blue]Contact us on Discord if you need this feature sooner.[/blue]") + console.print(f"šŸ”— [link]https://discord.gg/Q9P9AdHVHz[/link]") + + # Fallback to new agent creation + agent_id = generate_agent_id() + console.print(f"šŸ†” New Agent ID: [magenta]{agent_id}[/magenta]") + + elif choice == "new": + # Create new agent with new ID + agent_id = generate_agent_id() + console.print(f"šŸ†” New Agent ID: [magenta]{agent_id}[/magenta]") + + else: # cancel + return { + "success": False, + "error": "Upload cancelled by user", + "code": "USER_CANCELLED", + "existing_agent": existing_agent + } + else: + # No existing agent found - create new one + agent_id = generate_agent_id() + console.print(f"šŸ†” New Agent ID: [magenta]{agent_id}[/magenta]") - # Upload to server + # Step 5: Create zip file and upload in parallel console.print(f"🌐 Uploading to: [bold blue]{self.base_url}[/bold blue]") with Progress( SpinnerColumn(), - TextColumn("[bold green]Uploading...[/bold green]"), + TextColumn("[bold green]{task.description}[/bold green]"), BarColumn(bar_width=40), TextColumn("[bold]{task.percentage:>3.0f}%"), TimeElapsedColumn(), + TimeRemainingColumn(), console=console, ) as progress: -<<<<<<< HEAD - upload_task = progress.add_task("Uploading...", total=100) - result = self._upload_to_server_secure(zip_path, upload_metadata, progress, upload_task) -======= upload_task = progress.add_task("Initializing upload...", total=100) # Step 1: Upload metadata first @@ -646,12 +686,15 @@ def upload_agent(self, folder_path: str) -> Dict: # Step 3: Upload zip file result = self._upload_agent_zip_file_to_server(zip_path, agent_id, progress, upload_task) ->>>>>>> sawra/runagent_cloud_support # Clean up zip file os.unlink(zip_path) - return self._process_upload_result(result, upload_metadata) + return self._process_upload_result(result, { + "agent_id": agent_id, + "fingerprint": fingerprint, + "source_folder": str(folder_path) + }) except Exception as e: return {"success": False, "error": f"Upload failed: {str(e)}"} diff --git a/runagent/utils/agent_id.py b/runagent/utils/agent_id.py new file mode 100644 index 0000000..2490c3e --- /dev/null +++ b/runagent/utils/agent_id.py @@ -0,0 +1,184 @@ +""" +Agent ID generation and management utilities +""" +import hashlib +import uuid +from pathlib import Path +from typing import Optional, Dict, Any +import json +import os + + +def generate_agent_id() -> str: + """Generate a new UUID for agent ID""" + return str(uuid.uuid4()) + + +def generate_agent_fingerprint(folder_path: Path) -> str: + """ + Generate a fingerprint for an agent folder based on its contents. + This helps identify if the same agent is being uploaded multiple times. + """ + fingerprint_data = { + "files": {}, + "structure": [] + } + + # Get all files and their basic info + for file_path in sorted(folder_path.rglob("*")): + if file_path.is_file() and not file_path.name.startswith("."): + # Skip unnecessary files + if file_path.name in ["__pycache__", ".DS_Store", "Thumbs.db"]: + continue + if file_path.suffix in [".pyc", ".pyo", ".log"]: + continue + + relative_path = file_path.relative_to(folder_path) + fingerprint_data["files"][str(relative_path)] = { + "size": file_path.stat().st_size, + "mtime": file_path.stat().st_mtime + } + fingerprint_data["structure"].append(str(relative_path)) + + # Create hash from the fingerprint data + fingerprint_json = json.dumps(fingerprint_data, sort_keys=True) + return hashlib.sha256(fingerprint_json.encode()).hexdigest() + + +def get_agent_name_from_folder(folder_path: Path) -> str: + """Extract a meaningful agent name from the folder path""" + return folder_path.name + + +def get_framework_from_folder(folder_path: Path) -> Optional[str]: + """ + Try to detect framework from folder contents. + This is a simple heuristic - can be enhanced later. + """ + folder_path = Path(folder_path) + + # Check for common framework files + if (folder_path / "requirements.txt").exists(): + try: + with open(folder_path / "requirements.txt", "r") as f: + content = f.read().lower() + if "langchain" in content: + return "langchain" + elif "crewai" in content: + return "crewai" + elif "autogen" in content: + return "autogen" + elif "openai" in content: + return "openai" + except: + pass + + # Check for common framework directories/files + if (folder_path / "langchain").exists(): + return "langchain" + elif (folder_path / "crewai").exists(): + return "crewai" + elif (folder_path / "autogen").exists(): + return "autogen" + + # Check main.py for imports + main_py = folder_path / "main.py" + if main_py.exists(): + try: + with open(main_py, "r") as f: + content = f.read().lower() + if "from langchain" in content or "import langchain" in content: + return "langchain" + elif "from crewai" in content or "import crewai" in content: + return "crewai" + elif "from autogen" in content or "import autogen" in content: + return "autogen" + elif "openai" in content: + return "openai" + except: + pass + + return "unknown" + + +def get_agent_metadata(folder_path: Path, framework: Optional[str] = None) -> Dict[str, Any]: + """ + Extract comprehensive metadata from an agent folder + """ + folder_path = Path(folder_path) + + detected_framework = framework or get_framework_from_folder(folder_path) + + return { + "name": get_agent_name_from_folder(folder_path), + "description": f"Agent uploaded from {folder_path.name}", + "framework": detected_framework, + "template": "default", # Can be enhanced to detect actual template + "version": "1.0.0", + "fingerprint": generate_agent_fingerprint(folder_path), + "source_folder": str(folder_path), + "entrypoints": _detect_entrypoints(folder_path), + "env_vars": _detect_env_vars(folder_path) + } + + +def _detect_entrypoints(folder_path: Path) -> list: + """Detect entrypoints in the agent folder""" + entrypoints = [] + + # Look for main.py with common patterns + main_py = folder_path / "main.py" + if main_py.exists(): + try: + with open(main_py, "r") as f: + content = f.read() + + # Simple heuristic to find functions that could be entrypoints + import ast + try: + tree = ast.parse(content) + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef): + if any(keyword in node.name.lower() for keyword in ['main', 'run', 'execute', 'handle', 'process']): + entrypoints.append({ + "file": "main.py", + "module": node.name, + "tag": node.name.lower() + }) + except: + # Fallback to default entrypoints + entrypoints = [ + {"file": "main.py", "module": "main", "tag": "main"}, + {"file": "main.py", "module": "run", "tag": "run"} + ] + except: + pass + + # If no entrypoints found, provide defaults + if not entrypoints: + entrypoints = [ + {"file": "main.py", "module": "mock_response_stream", "tag": "minimal_stream"}, + {"file": "main.py", "module": "mock_response", "tag": "minimal"} + ] + + return entrypoints + + +def _detect_env_vars(folder_path: Path) -> Dict[str, str]: + """Detect environment variables from .env files or config""" + env_vars = {} + + # Check for .env file + env_file = folder_path / ".env" + if env_file.exists(): + try: + with open(env_file, "r") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, value = line.split("=", 1) + env_vars[key.strip()] = value.strip() + except: + pass + + return env_vars diff --git a/runagent/utils/enums/framework.py b/runagent/utils/enums/framework.py new file mode 100644 index 0000000..ba31fa3 --- /dev/null +++ b/runagent/utils/enums/framework.py @@ -0,0 +1,156 @@ +from enum import Enum +import typing as t + + +# Framework Enum with efficient caching +class Framework(Enum): + DEFAULT = "default" + AG2 = "ag2" + AGNO = "agno" + AUTOGEN = "autogen" + CREWAI = "crewai" + LANGCHAIN = "langchain" + LANGGRAPH = "langgraph" + LETTA = "letta" + LLAMAINDEX = "llamaindex" + OPENAI = "openai" + N8N = "n8n" + + @classmethod + def _pythonic_frameworks_cache(cls) -> t.FrozenSet['Framework']: + return frozenset({ + cls.AG2, cls.AGNO, cls.AUTOGEN, cls.CREWAI, + cls.LANGCHAIN, cls.LANGGRAPH, cls.LETTA, + cls.LLAMAINDEX, cls.OPENAI + }) + + @classmethod + def _webhook_frameworks_cache(cls) -> t.FrozenSet['Framework']: + return frozenset({cls.N8N}) + + @classmethod + def get_pythonic_frameworks(cls) -> t.FrozenSet['Framework']: + """Get all pythonic frameworks (cached)""" + return cls._pythonic_frameworks_cache() + + @classmethod + def get_webhook_frameworks(cls) -> t.FrozenSet['Framework']: + """Get all webhook frameworks (cached)""" + return cls._webhook_frameworks_cache() + + @classmethod + def get_selectable_frameworks(cls) -> t.List['Framework']: + """Get frameworks that can be selected (excluding DEFAULT)""" + return [f for f in cls if f != cls.DEFAULT] + + @classmethod + def from_string(cls, value: str) -> 'Framework': + """Convert string to Framework with validation""" + try: + return cls(value) + except ValueError: + valid_values = [f.value for f in cls] + raise ValueError(f"Invalid framework: '{value}'. Valid options: {valid_values}") + + @classmethod + def validate_framework_str(cls, value: str) -> bool: + """Check if string is a valid framework""" + try: + cls(value) + return True + except ValueError: + return False + + def is_pythonic(self) -> bool: + """Check if this framework is pythonic""" + return self in self.get_pythonic_frameworks() + + def is_webhook(self) -> bool: + """Check if this framework is webhook""" + return self in self.get_webhook_frameworks() + + def is_default(self) -> bool: + """Check if this framework is the default""" + return self == self.DEFAULT + + @property + def category(self) -> str: + """Get the category of this framework""" + if self.is_default(): + return "default" + elif self.is_pythonic(): + return "pythonic" + elif self.is_webhook(): + return "webhook" + else: + return "unknown" + # Class methods for string validation and conversion + @classmethod + def from_string(cls, framework_str: str) -> 'Framework': + """Convert string to Framework enum with validation""" + try: + return cls(framework_str) + except ValueError: + valid_frameworks = [f.value for f in cls] + raise ValueError(f"Invalid framework: '{framework_str}'. Valid options: {valid_frameworks}") + + @classmethod + def is_valid_framework_string(cls, framework_str: str) -> bool: + """Check if string is a valid framework""" + try: + cls(framework_str) + return True + except ValueError: + return False + + # @classmethod + # def is_pythonic_string(cls, framework_str: str) -> bool: + # """Check if string represents a pythonic framework""" + # try: + # framework = cls(framework_str) + # return framework.is_pythonic() + # except ValueError: + # return False + + # @classmethod + # def is_webhook_string(cls, framework_str: str) -> bool: + # """Check if string represents a webhook framework""" + # try: + # framework = cls(framework_str) + # return framework.is_webhook() + # except ValueError: + # return False + + # @classmethod + # def is_default_string(cls, framework_str: str) -> bool: + # """Check if string represents the default framework""" + # try: + # framework = cls(framework_str) + # return framework.is_default() + # except ValueError: + # return False + + @classmethod + def from_str(cls, framework_str: str) -> str: + """Get category from string""" + try: + framework = cls(framework_str) + return framework.category + except ValueError: + return "invalid" + + # # Utility class methods + # @classmethod + # def get_all_framework_values(cls) -> t.List[str]: + # """Get all framework values as strings""" + # return [f.value for f in cls] + + # @classmethod + # def get_pythonic_framework_values(cls) -> t.List[str]: + # """Get pythonic framework values as strings""" + # return [f.value for f in cls.get_pythonic_frameworks()] + + # @classmethod + # def get_webhook_framework_values(cls) -> t.List[str]: + # """Get webhook framework values as strings""" + # return [f.value for f in cls.get_webhook_frameworks()] From f4699b0aab04ffcc923d8b8d37877732a0703297 Mon Sep 17 00:00:00 2001 From: sawradip Date: Fri, 26 Sep 2025 03:02:14 +0600 Subject: [PATCH 3/4] feat: release scripts updated --- release.sh | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/release.sh b/release.sh index 13e660d..e9a75ad 100755 --- a/release.sh +++ b/release.sh @@ -370,14 +370,16 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 0 fi +# Generate changelog BEFORE creating tag +generate_changelog + # Handle existing tag if handle_existing_tag "$VERSION"; then echo "āœ… Tag v$VERSION updated successfully!" - generate_changelog exit 0 fi -# Stage and commit changes +# Stage and commit changes (including changelog) git add . if git diff --staged --quiet; then @@ -385,23 +387,29 @@ if git diff --staged --quiet; then exit 1 fi +# Commit changes first +git commit -m "chore: bump version to v$VERSION + +- Updated all SDK versions to $VERSION +- Generated changelog with git-cliff" -q + # Create new tag git tag -a "v$VERSION" -m "Release v$VERSION -RunAgent Universal Release v$VERSION" +RunAgent Universal Release v$VERSION -git push --tag -echo "āœ… Tag v$VERSION created and pushed successfully!" +All SDKs updated to version $VERSION" -generate_changelog +CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null) -git commit -m "chore: bump version to v$VERSION +# Push commit first, then tag (to prevent orphaned tags) +echo "šŸ“¤ Pushing commit to origin/$CURRENT_BRANCH..." +git push origin "$CURRENT_BRANCH" -- Updated all SDK versions to $VERSION -- Generated changelog with git-cliff" -q +echo "šŸ“¤ Pushing tag v$VERSION..." +git push origin "v$VERSION" -CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null) +echo "āœ… Release v$VERSION completed successfully!" echo "" echo "šŸ“‹ Next steps:" -echo " 1. Push changes: git push origin $CURRENT_BRANCH" -echo " 2. Push tag: git push origin v$VERSION" -echo " 3. Monitor workflows at: https://github.com/runagent-dev/runagent/actions" \ No newline at end of file +echo " 1. Monitor workflows at: https://github.com/runagent-dev/runagent/actions" +echo " 2. Verify release at: https://github.com/runagent-dev/runagent/releases" \ No newline at end of file From edc64465728cb65e3164c102d76884fe972c81aa Mon Sep 17 00:00:00 2001 From: sawradip Date: Fri, 26 Sep 2025 03:02:30 +0600 Subject: [PATCH 4/4] chore: bump version to v0.1.23 - Updated all SDK versions to 0.1.23 - Generated changelog with git-cliff --- CHANGELOG.md | 31 ++++++++++++++++++++++--------- pyproject.toml | 6 +++--- runagent-go/runagent/version.go | 2 +- runagent-rust/runagent/Cargo.toml | 2 +- runagent-ts/package-lock.json | 4 ++-- runagent-ts/package.json | 2 +- runagent/__init__.py | 2 +- runagent/__version__.py | 4 ++-- 8 files changed, 33 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d972773..28ebd79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,35 @@ # Changelog All notable changes to this project's latest version. -## [0.1.21] - 2025-09-23 +## [0.1.22] - 2025-09-25 ### Bug Fixes -- Fixed templatemanger checking all templates +- Langgraph tempate config added +- Langchain advanced template removed +- Release script fixed ### Features -- Added framework enum -- Framework enum support -- Agent upload working -- Restclient agent/start working with new response format +- Template init bugged fixed -### Refactor +### Miscellaneous Tasks -- Disabled deploy-local -- Remove deply-local p2 +- Bump version to v0.1.20 +- Bump version to v0.1.22 + +## [0.1.20] - 2025-08-27 + +### Documentation + +- Added changelog + +### Features + +- Added version command to see package version ([#60](https://github.com/your-org/your-repo/issues/60)) + +### Miscellaneous Tasks + +- Bump version to v0.1.21 diff --git a/pyproject.toml b/pyproject.toml index b14138d..d0560df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "runagent" -version = "0.1.21" +version = "0.1.23" description = "A command-line tool and SDK for deploying, managing, and interacting with AI agents" readme = "README.md" requires-python = ">=3.9" @@ -103,7 +103,7 @@ line_length = 88 skip = ["docs"] [tool.mypy] -python_version = "0.1.21" +python_version = "0.1.23" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true @@ -159,7 +159,7 @@ fail_under = 80 [tool.ruff] line-length = 88 -target-version = "0.1.21" +target-version = "0.1.23" select = [ "E", # pycodestyle errors "W", # pycodestyle warnings diff --git a/runagent-go/runagent/version.go b/runagent-go/runagent/version.go index 158b463..f07bbbf 100644 --- a/runagent-go/runagent/version.go +++ b/runagent-go/runagent/version.go @@ -1,4 +1,4 @@ package runagent // Version represents the current version of the RunAgent Go SDK -const Version = "0.1.21" +const Version = "0.1.23" diff --git a/runagent-rust/runagent/Cargo.toml b/runagent-rust/runagent/Cargo.toml index 84ddee2..7cf50f2 100644 --- a/runagent-rust/runagent/Cargo.toml +++ b/runagent-rust/runagent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runagent" -version = "0.1.21" +version = "0.1.23" edition = "2021" description = "RunAgent SDK for Rust - Deploy and manage AI agents easily" license = "MIT" diff --git a/runagent-ts/package-lock.json b/runagent-ts/package-lock.json index b69b847..019b417 100644 --- a/runagent-ts/package-lock.json +++ b/runagent-ts/package-lock.json @@ -1,12 +1,12 @@ { "name": "runagent", - "version": "0.1.21", + "version": "0.1.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "runagent", - "version": "0.1.21", + "version": "0.1.23", "dependencies": { "better-sqlite3": "^12.2.0" }, diff --git a/runagent-ts/package.json b/runagent-ts/package.json index ad0f182..96b747b 100644 --- a/runagent-ts/package.json +++ b/runagent-ts/package.json @@ -1,6 +1,6 @@ { "name": "runagent", - "version": "0.1.21", + "version": "0.1.23", "type": "module", "files": [ "dist" diff --git a/runagent/__init__.py b/runagent/__init__.py index 026367e..550e3ab 100644 --- a/runagent/__init__.py +++ b/runagent/__init__.py @@ -5,7 +5,7 @@ built with frameworks like LangGraph, LangChain, and LlamaIndex. """ -__version__ = "0.1.20" +__version__ = "0.1.23" from .client import RunAgentClient diff --git a/runagent/__version__.py b/runagent/__version__.py index cd76ce3..a1e8857 100644 --- a/runagent/__version__.py +++ b/runagent/__version__.py @@ -1,5 +1,5 @@ <<<<<<< HEAD -__version__ = "0.1.20" +__version__ = "0.1.23" ======= -__version__ = "0.1.21" +__version__ = "0.1.23" >>>>>>> sawra/runagent_cloud_support