summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuval Adam <_@yuv.al>2025-03-06 14:25:21 +0100
committerYuval Adam <_@yuv.al>2025-03-06 14:25:21 +0100
commit86be7ada89bf2a4a29d1c6a6ffc60d790e0d5e7a (patch)
treee3145797794e8c431e7942c7c846753b587e595a
parent6e010653dbce4e49a88a2da02fb814786f7bf5b5 (diff)
Add dynamic capabilities, test on two basics
-rw-r--r--strangeloop/__init__.py6
-rw-r--r--strangeloop/capabilities/__init__.py8
-rw-r--r--strangeloop/capabilities/generate_secure_password.py52
-rw-r--r--strangeloop/capabilities/get_public_ip_address.py36
-rw-r--r--strangeloop/cli.py268
-rw-r--r--strangeloop/dynamic.py107
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