summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md72
-rw-r--r--strangeloop/cli.py236
2 files changed, 308 insertions, 0 deletions
diff --git a/README.md b/README.md
index 04ef760..754ea63 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,16 @@ uvx strangeloop process FILE_PATH [--output OUTPUT_PATH]
# Ask Claude Sonnet 3.7 a question
uvx strangeloop ask "What is recursive self-improvement in AI?"
+# Use the AI agent loop to fulfill requests
+uvx strangeloop do "find the current weather in New York"
+uvx strangeloop do "create a function to generate secure passwords"
+
+# Manage capabilities
+uvx strangeloop capability add "generate a secure random password"
+uvx strangeloop capability list
+uvx strangeloop capability show generate_secure_password
+uvx strangeloop capability run generate_secure_password 16 --include-special-chars
+
# Configuration management
uvx strangeloop config set anthropic_api_key "your-api-key"
uvx strangeloop config get anthropic_api_key
@@ -49,6 +59,68 @@ strangeloop info
strangeloop ask "What is the meaning of life?" --max-tokens 2048 --temperature 0.8
```
+## AI Agent Loop
+
+The core of Strangeloop is the AI agent loop, which allows you to make requests in natural language and have the system determine the best way to fulfill them:
+
+```bash
+strangeloop do "find information about the weather in Paris"
+```
+
+The system will:
+1. Analyze your request
+2. Check available capabilities
+3. Either:
+ - Use an existing capability
+ - Create a new capability
+ - Provide a direct response
+
+You can control the execution with options:
+```bash
+# Don't automatically execute the suggested action
+strangeloop do "analyze this log file" --no-auto-execute
+
+# Adjust the temperature for more creative responses
+strangeloop do "write a poem about AI" --temperature 0.9
+```
+
+## Capabilities Management
+
+Strangeloop allows you to create, manage, and execute capabilities - Python functions that can be dynamically added to the system:
+
+### Adding Capabilities
+
+```bash
+# Add a new capability
+strangeloop capability add "generate a secure random password"
+
+# Control the generation parameters
+strangeloop capability add "fetch current weather for a location" --temperature 0.5 --max-tokens 4096
+```
+
+### Listing and Viewing Capabilities
+
+```bash
+# List all available capabilities
+strangeloop capability list
+
+# Get detailed information about capabilities
+strangeloop capability list --verbose
+
+# Show details of a specific capability
+strangeloop capability show generate_secure_password
+```
+
+### Running Capabilities
+
+```bash
+# Run a capability with arguments
+strangeloop capability run generate_secure_password 16 true
+
+# Parse arguments as JSON
+strangeloop capability run fetch_weather "New York" --json
+```
+
## API Keys and Configuration
To use the Claude Sonnet 3.7 integration, you need to provide your Anthropic API key in one of these ways (in order of precedence):
diff --git a/strangeloop/cli.py b/strangeloop/cli.py
index 3409165..e85744f 100644
--- a/strangeloop/cli.py
+++ b/strangeloop/cli.py
@@ -7,7 +7,9 @@ import sys
import json
import importlib
import inspect
+import textwrap
from pathlib import Path
+from typing import Dict, Any, List, Callable, Optional, Tuple
from .llm import ask_claude
from .config import get_config
@@ -433,5 +435,239 @@ def config_path():
sys.exit(1)
+@cli.command()
+@click.argument("request", required=True, nargs=-1)
+@click.option("--max-tokens", "-m", default=4096, help="Maximum tokens in response")
+@click.option("--temperature", "-t", default=0.7, type=float, help="Temperature (0.0-1.0)")
+@click.option("--auto-execute/--no-auto-execute", default=True, help="Automatically execute the suggested action")
+def do(request, max_tokens, temperature, auto_execute):
+ """
+ Execute an AI agent loop to fulfill a request using available capabilities.
+
+ REQUEST is what you want strangeloop to do for you.
+ """
+ try:
+ # Convert request tuple to string
+ request_str = " ".join(request)
+ click.echo(f"Processing request: {request_str}")
+
+ # Get available capabilities
+ capabilities_info = get_available_capabilities()
+
+ if not capabilities_info:
+ click.echo("No capabilities available. Creating a new capability...")
+ if auto_execute:
+ click.echo("Automatically creating a new capability to handle your request.")
+ ctx = click.get_current_context()
+ return ctx.invoke(capability_add, description=request_str,
+ max_tokens=max_tokens, temperature=temperature, save=True)
+ else:
+ click.echo("Use 'strangeloop capability add' to create a new capability.")
+ return
+
+ # Format capabilities for the prompt
+ capabilities_text = format_capabilities_for_prompt(capabilities_info)
+
+ # Prepare the prompt for Claude
+ prompt = f"""
+ # Request
+ The user has requested: "{request_str}"
+
+ # Available Capabilities
+ You have the following capabilities available:
+
+ {capabilities_text}
+
+ # Your Task
+ Analyze the request and determine the best course of action:
+
+ 1. If an existing capability can handle the request (or part of it), respond with a JSON object like this:
+ {{
+ "action": "use_capability",
+ "capability": "capability_name",
+ "arguments": ["arg1", "arg2", ...],
+ "explanation": "Why this capability is appropriate"
+ }}
+
+ 2. If the request requires a new capability, respond with a JSON object like this:
+ {{
+ "action": "create_capability",
+ "description": "Detailed description of the capability needed",
+ "explanation": "Why a new capability is needed"
+ }}
+
+ 3. If the request can be answered directly without using or creating capabilities, respond with:
+ {{
+ "action": "direct_response",
+ "response": "Your detailed response to the request",
+ "explanation": "Why a direct response is sufficient"
+ }}
+
+ Respond ONLY with a valid JSON object matching one of these formats. Do not include any other text.
+ """
+
+ click.echo("Consulting Claude to determine the best approach...")
+ response = ask_claude(prompt, max_tokens, temperature)
+
+ # Parse the JSON response
+ try:
+ # Clean up the response if needed (remove markdown code blocks)
+ response = response.strip()
+ if response.startswith("```json"):
+ response = response[len("```json"):].strip()
+ if response.startswith("```"):
+ response = response[len("```"):].strip()
+ if response.endswith("```"):
+ response = response[:-len("```")].strip()
+
+ action_plan = json.loads(response)
+
+ # Display the explanation
+ if "explanation" in action_plan:
+ click.echo(f"\nReasoning: {action_plan['explanation']}")
+
+ # Execute the appropriate action
+ action = action_plan.get("action")
+
+ if action == "use_capability":
+ capability_name = action_plan.get("capability")
+ arguments = action_plan.get("arguments", [])
+
+ click.echo(f"\nSuggested action: Use capability '{capability_name}' with arguments: {arguments}")
+
+ if auto_execute:
+ click.echo("Automatically executing the suggested capability...")
+ ctx = click.get_current_context()
+ return ctx.invoke(capability_run, name=capability_name, args=arguments, json=False)
+ else:
+ click.echo("\nTo execute this capability, run:")
+ args_str = " ".join([f'"{arg}"' for arg in arguments])
+ click.echo(f" strangeloop capability run {capability_name} {args_str}")
+
+ elif action == "create_capability":
+ description = action_plan.get("description")
+
+ click.echo(f"\nSuggested action: Create a new capability with description:")
+ click.echo(f" {description}")
+
+ if auto_execute:
+ click.echo("Automatically creating the suggested capability...")
+ ctx = click.get_current_context()
+ return ctx.invoke(capability_add, description=description,
+ max_tokens=max_tokens, temperature=temperature, save=True)
+ else:
+ click.echo("\nTo create this capability, run:")
+ click.echo(f' strangeloop capability add "{description}"')
+
+ elif action == "direct_response":
+ direct_response = action_plan.get("response", "")
+
+ click.echo("\nDirect response:")
+ click.echo(textwrap.fill(direct_response, width=80))
+
+ else:
+ click.echo(f"\nUnknown action type: {action}")
+ click.echo("Full response from Claude:")
+ click.echo(response)
+
+ except json.JSONDecodeError:
+ click.echo("Could not parse Claude's response as JSON. Full response:")
+ click.echo(response)
+ sys.exit(1)
+
+ except Exception as e:
+ click.echo(f"Error processing request: {str(e)}", err=True)
+ sys.exit(1)
+
+
+def get_available_capabilities() -> List[Dict[str, Any]]:
+ """
+ Get information about all available capabilities.
+
+ Returns:
+ List of dictionaries with capability information
+ """
+ capabilities_info = []
+
+ try:
+ # Import capabilities module
+ try:
+ import strangeloop.capabilities as capabilities
+ importlib.reload(capabilities) # Reload to catch any new capabilities
+ except ImportError:
+ return capabilities_info
+
+ # Get all functions from the capabilities module
+ for name in dir(capabilities):
+ if name.startswith('_'):
+ continue
+
+ obj = getattr(capabilities, name)
+ if inspect.isfunction(obj):
+ # Get function signature
+ sig = inspect.signature(obj)
+
+ # Get docstring
+ doc = inspect.getdoc(obj) or "No documentation"
+
+ # Add to capabilities list
+ capabilities_info.append({
+ "name": name,
+ "signature": str(sig),
+ "docstring": doc,
+ "parameters": [
+ {
+ "name": param_name,
+ "annotation": str(param.annotation) if param.annotation != inspect.Parameter.empty else "Any",
+ "default": None if param.default == inspect.Parameter.empty else param.default,
+ "required": param.default == inspect.Parameter.empty and param.kind not in (
+ inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
+ }
+ for param_name, param in sig.parameters.items()
+ ]
+ })
+
+ except Exception as e:
+ click.echo(f"Warning: Error getting capabilities: {str(e)}", err=True)
+
+ return capabilities_info
+
+
+def format_capabilities_for_prompt(capabilities_info: List[Dict[str, Any]]) -> str:
+ """
+ Format capabilities information for inclusion in a prompt.
+
+ Args:
+ capabilities_info: List of dictionaries with capability information
+
+ Returns:
+ Formatted string describing capabilities
+ """
+ if not capabilities_info:
+ return "No capabilities available."
+
+ formatted_text = ""
+
+ for cap in capabilities_info:
+ # Format the first line of the docstring
+ doc_first_line = cap["docstring"].split('\n')[0]
+
+ # Format the capability
+ formatted_text += f"- {cap['name']}{cap['signature']}\n"
+ formatted_text += f" Description: {doc_first_line}\n"
+
+ # Add parameter details
+ if cap["parameters"]:
+ formatted_text += " Parameters:\n"
+ for param in cap["parameters"]:
+ required = " (required)" if param["required"] else ""
+ default = f" (default: {param['default']})" if param["default"] is not None else ""
+ formatted_text += f" - {param['name']}: {param['annotation']}{required}{default}\n"
+
+ formatted_text += "\n"
+
+ return formatted_text.strip()
+
+
if __name__ == "__main__":
cli()