diff options
| author | Yuval Adam <_@yuv.al> | 2025-03-06 14:34:00 +0100 |
|---|---|---|
| committer | Yuval Adam <_@yuv.al> | 2025-03-06 14:34:00 +0100 |
| commit | 6b82841a7042fcd08ec328a0b52cff4e1c9479c9 (patch) | |
| tree | 8c97a02e7ca6f2ba66119f32e4899131aae0d25b | |
| parent | 86be7ada89bf2a4a29d1c6a6ffc60d790e0d5e7a (diff) | |
Implement core do() step
| -rw-r--r-- | README.md | 72 | ||||
| -rw-r--r-- | strangeloop/cli.py | 236 |
2 files changed, 308 insertions, 0 deletions
@@ -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() |
