diff options
| author | Yuval Adam <_@yuv.al> | 2025-03-06 14:25:21 +0100 |
|---|---|---|
| committer | Yuval Adam <_@yuv.al> | 2025-03-06 14:25:21 +0100 |
| commit | 86be7ada89bf2a4a29d1c6a6ffc60d790e0d5e7a (patch) | |
| tree | e3145797794e8c431e7942c7c846753b587e595a | |
| parent | 6e010653dbce4e49a88a2da02fb814786f7bf5b5 (diff) | |
Add dynamic capabilities, test on two basics
| -rw-r--r-- | strangeloop/__init__.py | 6 | ||||
| -rw-r--r-- | strangeloop/capabilities/__init__.py | 8 | ||||
| -rw-r--r-- | strangeloop/capabilities/generate_secure_password.py | 52 | ||||
| -rw-r--r-- | strangeloop/capabilities/get_public_ip_address.py | 36 | ||||
| -rw-r--r-- | strangeloop/cli.py | 268 | ||||
| -rw-r--r-- | strangeloop/dynamic.py | 107 |
6 files changed, 477 insertions, 0 deletions
diff --git a/strangeloop/__init__.py b/strangeloop/__init__.py index a1cf17f..50d4717 100644 --- a/strangeloop/__init__.py +++ b/strangeloop/__init__.py @@ -8,3 +8,9 @@ try: __version__ = version("strangeloop") except Exception: pass + +# Import capabilities if they exist +try: + from .capabilities import * +except ImportError: + pass diff --git a/strangeloop/capabilities/__init__.py b/strangeloop/capabilities/__init__.py new file mode 100644 index 0000000..a1c9c5a --- /dev/null +++ b/strangeloop/capabilities/__init__.py @@ -0,0 +1,8 @@ +""" +Dynamically generated capabilities for Strangeloop. +This package contains functions that have been dynamically added at runtime. +""" + +from strangeloop.capabilities.generate_secure_password import generate_secure_password + +from strangeloop.capabilities.get_public_ip_address import get_public_ip_address diff --git a/strangeloop/capabilities/generate_secure_password.py b/strangeloop/capabilities/generate_secure_password.py new file mode 100644 index 0000000..86c040f --- /dev/null +++ b/strangeloop/capabilities/generate_secure_password.py @@ -0,0 +1,52 @@ +""" +Dynamically generated capability: generate_secure_password +""" + +import random +import string +from typing import Optional + + +def generate_secure_password(length: int = 16, include_uppercase: bool = True, + include_lowercase: bool = True, include_digits: bool = True, + include_special_chars: bool = True, + special_chars: str = "!@#$%^&*()_-+=<>?/[]{}|") -> str: + """ + Generate a random secure password with configurable length and character types. + + Args: + length: The length of the password to generate. Defaults to 16. + include_uppercase: Whether to include uppercase letters. Defaults to True. + include_lowercase: Whether to include lowercase letters. Defaults to True. + include_digits: Whether to include digits. Defaults to True. + include_special_chars: Whether to include special characters. Defaults to True. + special_chars: String containing special characters to use. Defaults to common special characters. + + Returns: + A randomly generated password string with the specified characteristics. + + Raises: + ValueError: If length is less than 1 or if all character type options are False. + """ + if length < 1: + raise ValueError("Password length must be at least 1 character") + + # Prepare the character pool based on selected options + char_pool = "" + + if include_uppercase: + char_pool += string.ascii_uppercase + if include_lowercase: + char_pool += string.ascii_lowercase + if include_digits: + char_pool += string.digits + if include_special_chars: + char_pool += special_chars + + if not char_pool: + raise ValueError("At least one character type must be selected") + + # Generate the password + password = ''.join(random.choice(char_pool) for _ in range(length)) + + return password
\ No newline at end of file diff --git a/strangeloop/capabilities/get_public_ip_address.py b/strangeloop/capabilities/get_public_ip_address.py new file mode 100644 index 0000000..0728dc6 --- /dev/null +++ b/strangeloop/capabilities/get_public_ip_address.py @@ -0,0 +1,36 @@ +""" +Dynamically generated capability: get_public_ip_address +""" + +import requests +from typing import Optional + + +def get_public_ip_address() -> Optional[str]: + """ + Retrieves the public IP address of the current machine by calling httpbin.org/ip. + + Returns: + Optional[str]: The public IP address as a string if successful, None otherwise. + + Raises: + requests.RequestException: If there's an error with the HTTP request. + ValueError: If the response format is unexpected. + """ + try: + response = requests.get("https://httpbin.org/ip", timeout=10) + response.raise_for_status() # Raise an exception for HTTP errors + + data = response.json() + if "origin" not in data: + raise ValueError("Unexpected response format from httpbin.org/ip") + + return data["origin"] + except requests.RequestException as e: + # Log the error or handle it as needed + print(f"Error retrieving public IP address: {e}") + return None + except ValueError as e: + # Log the error or handle it as needed + print(f"Error parsing response: {e}") + return None
\ No newline at end of file diff --git a/strangeloop/cli.py b/strangeloop/cli.py index 9b4900d..3409165 100644 --- a/strangeloop/cli.py +++ b/strangeloop/cli.py @@ -5,6 +5,8 @@ Strangeloop CLI - A recursive and self-referential AI agent framework. import click import sys import json +import importlib +import inspect from pathlib import Path from .llm import ask_claude from .config import get_config @@ -78,6 +80,272 @@ def ask(question, max_tokens, temperature): @cli.group() +def capability(): + """Manage strangeloop capabilities.""" + pass + + +@capability.command(name="add") +@click.argument("description", required=True) +@click.option("--max-tokens", "-m", default=4096, help="Maximum tokens in response") +@click.option("--temperature", "-t", default=0.5, type=float, help="Temperature (0.0-1.0)") +@click.option("--save/--no-save", "-s/-n", default=True, help="Save the function to a file (default: save)") +def capability_add(description, max_tokens, temperature, save): + """ + Add a new capability using Claude and dynamically add it to strangeloop. + + DESCRIPTION is a description of what the function should do. + """ + try: + from .dynamic import add_function_to_module, save_function_to_file + import strangeloop + + # Prepare the prompt for Claude + prompt = f""" + Implement a Python function based on this capability description: + + {description} + + Requirements: + 1. Write a single, well-documented Python function with clear docstrings + 2. Include proper type hints + 3. Include appropriate error handling + 4. Make the function name descriptive of its purpose + 5. Only return the function code, nothing else + """ + + click.echo(f"Asking Claude to implement: {description}") + function_code = ask_claude(prompt, max_tokens, temperature) + + # Clean up the response if needed (remove markdown code blocks) + function_code = function_code.strip() + if function_code.startswith("```python"): + function_code = function_code[len("```python"):].strip() + if function_code.startswith("```"): + function_code = function_code[len("```"):].strip() + if function_code.endswith("```"): + function_code = function_code[:-len("```")].strip() + + # Display the generated function + click.echo("\nGenerated function:") + click.echo(function_code) + + # Add the function to the strangeloop module + try: + function = add_function_to_module("strangeloop", function_code) + function_name = function.__name__ + click.echo(f"\nSuccessfully added function '{function_name}' to strangeloop") + + # Save the function to a file if requested + if save: + file_path = save_function_to_file(function_code) + click.echo(f"Saved function to {file_path}") + + # Add import to __init__.py to make it available in future sessions + capabilities_init = Path(__file__).parent / "capabilities" / "__init__.py" + with open(capabilities_init, "a") as f: + f.write(f"\nfrom strangeloop.capabilities.{function_name} import {function_name}\n") + + click.echo(f"Added import to capabilities/__init__.py for future sessions") + + # Show usage example + click.echo("\nUsage example:") + click.echo(f" from strangeloop import {function_name}") + click.echo(f" help({function_name}) # View documentation") + click.echo(f" # Or use the CLI:") + click.echo(f" strangeloop capability run {function_name} [ARGS...]") + + except Exception as e: + click.echo(f"Error adding function to strangeloop: {str(e)}", err=True) + sys.exit(1) + + except Exception as e: + click.echo(f"Error implementing capability: {str(e)}", err=True) + sys.exit(1) + + +@capability.command(name="list") +@click.option("--verbose", "-v", is_flag=True, help="Show detailed information about each capability") +def capability_list(verbose): + """List all available capabilities.""" + try: + # Import capabilities module + try: + import strangeloop.capabilities as capabilities + importlib.reload(capabilities) # Reload to catch any new capabilities + except ImportError: + click.echo("No capabilities found.") + return + + # Get all functions from the capabilities module + functions = [] + for name in dir(capabilities): + if name.startswith('_'): + continue + + obj = getattr(capabilities, name) + if inspect.isfunction(obj): + functions.append((name, obj)) + + if not functions: + click.echo("No capabilities found.") + return + + click.echo(f"Found {len(functions)} capabilities:") + for name, func in sorted(functions, key=lambda x: x[0]): + if verbose: + # Get the first line of the docstring + doc = inspect.getdoc(func) or "No documentation" + doc_first_line = doc.split('\n')[0] + + # Get the function signature + sig = str(inspect.signature(func)) + + click.echo(f"\n{name}{sig}") + click.echo(f" {doc_first_line}") + + # Show file location + try: + file_path = inspect.getfile(func) + click.echo(f" Defined in: {file_path}") + except (TypeError, OSError): + pass + else: + click.echo(f"- {name}") + + if not verbose: + click.echo("\nUse --verbose for more details.") + click.echo("Use 'strangeloop capability show <name>' to see full documentation.") + + except Exception as e: + click.echo(f"Error listing capabilities: {str(e)}", err=True) + sys.exit(1) + + +@capability.command(name="show") +@click.argument("name", required=True) +def capability_show(name): + """Show detailed information about a specific capability.""" + try: + # Import capabilities module + try: + import strangeloop.capabilities as capabilities + importlib.reload(capabilities) # Reload to catch any new capabilities + except ImportError: + click.echo("No capabilities found.") + return + + # Get the function + if not hasattr(capabilities, name): + click.echo(f"Capability '{name}' not found.") + return + + func = getattr(capabilities, name) + if not inspect.isfunction(func): + click.echo(f"'{name}' is not a function capability.") + return + + # Display function information + click.echo(f"Capability: {name}{inspect.signature(func)}") + + # Show docstring + doc = inspect.getdoc(func) or "No documentation" + click.echo("\nDocumentation:") + click.echo(doc) + + # Show source code + try: + source = inspect.getsource(func) + click.echo("\nSource Code:") + click.echo(source) + except (TypeError, OSError) as e: + click.echo(f"\nCould not retrieve source code: {str(e)}") + + # Show file location + try: + file_path = inspect.getfile(func) + click.echo(f"\nDefined in: {file_path}") + except (TypeError, OSError): + pass + + # Show usage example + click.echo("\nUsage example:") + click.echo(f" from strangeloop import {name}") + click.echo(f" result = {name}(...)") + click.echo(f" # Or use the CLI:") + click.echo(f" strangeloop capability run {name} [ARGS...]") + + except Exception as e: + click.echo(f"Error showing capability: {str(e)}", err=True) + sys.exit(1) + + +@capability.command(name="run") +@click.argument("name", required=True) +@click.argument("args", nargs=-1) +@click.option("--json", "-j", is_flag=True, help="Parse arguments as JSON") +def capability_run(name, args, json): + """ + Run a capability with the given arguments. + + NAME is the name of the capability to run. + ARGS are the arguments to pass to the capability. + """ + try: + # Import capabilities module + try: + import strangeloop.capabilities as capabilities + importlib.reload(capabilities) # Reload to catch any new capabilities + except ImportError: + click.echo("No capabilities found.") + return + + # Get the function + if not hasattr(capabilities, name): + click.echo(f"Capability '{name}' not found.") + return + + func = getattr(capabilities, name) + if not inspect.isfunction(func): + click.echo(f"'{name}' is not a function capability.") + return + + # Parse arguments + parsed_args = [] + parsed_kwargs = {} + + if json: + # Parse all arguments as JSON + import json as json_module + for arg in args: + try: + parsed_args.append(json_module.loads(arg)) + except json_module.JSONDecodeError: + # If not valid JSON, use as string + parsed_args.append(arg) + else: + # Simple string arguments + parsed_args = args + + # Run the function + click.echo(f"Running capability '{name}'...") + result = func(*parsed_args, **parsed_kwargs) + + # Display the result + click.echo("\nResult:") + if result is None: + click.echo("(No return value)") + elif isinstance(result, (dict, list)): + click.echo(json.dumps(result, indent=2)) + else: + click.echo(result) + + except Exception as e: + click.echo(f"Error running capability: {str(e)}", err=True) + sys.exit(1) + + +@cli.group() def config(): """Manage Strangeloop configuration.""" pass diff --git a/strangeloop/dynamic.py b/strangeloop/dynamic.py new file mode 100644 index 0000000..65b2655 --- /dev/null +++ b/strangeloop/dynamic.py @@ -0,0 +1,107 @@ +""" +Dynamic code loading and execution module for Strangeloop. +Provides functionality to dynamically add code to the running instance. +""" +import importlib.util +import sys +import types +import inspect +from pathlib import Path +from typing import Any, Dict, Optional, Callable + + +def add_function_to_module(module_name: str, function_code: str, function_name: Optional[str] = None) -> Callable: + """ + Dynamically add a function to a module in the current Python process. + + Args: + module_name: The name of the module to add the function to + function_code: The Python code for the function + function_name: Optional name to extract from the function code + (if None, will try to parse from the code) + + Returns: + The function object that was added + + Raises: + ValueError: If function_name cannot be determined or module doesn't exist + SyntaxError: If the function code has syntax errors + """ + # Get the module + if module_name not in sys.modules: + raise ValueError(f"Module '{module_name}' not found in sys.modules") + + module = sys.modules[module_name] + + # Create a new namespace for executing the function code + namespace: Dict[str, Any] = {} + + # Execute the function code in the namespace + try: + exec(function_code, namespace) + except SyntaxError as e: + raise SyntaxError(f"Syntax error in function code: {str(e)}") + + # If function_name is not provided, try to extract it from the code + if function_name is None: + # Look for function definitions in the namespace + functions = [name for name, obj in namespace.items() + if inspect.isfunction(obj)] + + if not functions: + raise ValueError("No function found in the provided code") + + function_name = functions[0] + + # Get the function from the namespace + if function_name not in namespace: + raise ValueError(f"Function '{function_name}' not found in the provided code") + + function = namespace[function_name] + + # Add the function to the module + setattr(module, function_name, function) + + return function + + +def save_function_to_file(function_code: str, directory: Optional[Path] = None) -> Path: + """ + Save a dynamically created function to a file in the capabilities directory. + + Args: + function_code: The Python code for the function + directory: Optional directory to save the file (defaults to capabilities) + + Returns: + Path to the saved file + """ + # Extract function name from the code + import re + match = re.search(r"def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", function_code) + if not match: + raise ValueError("Could not extract function name from code") + + function_name = match.group(1) + + # Determine the directory to save the file + if directory is None: + # Get the strangeloop package directory + package_dir = Path(__file__).parent + directory = package_dir / "capabilities" + + # Create the directory if it doesn't exist + directory.mkdir(parents=True, exist_ok=True) + + # Create the file path + file_path = directory / f"{function_name}.py" + + # Add module docstring if not present + if not function_code.strip().startswith('"""'): + function_code = f'"""\nDynamically generated capability: {function_name}\n"""\n\n{function_code}' + + # Write the function code to the file + with open(file_path, "w") as f: + f.write(function_code) + + return file_path |
