This commit is contained in:
marko-kraemer 2024-11-02 00:05:29 +01:00
parent 0b6f6fd103
commit 71515f506d
14 changed files with 380 additions and 159 deletions

View File

@ -1,4 +1,3 @@
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GROQ_API_KEY=

230
README.md
View File

@ -1,139 +1,163 @@
# AgentPress: LLM Messages[] API on Steroids called "Threads" with automatic Tool Execution and simple state management.
# AgentPress: Building Blocks for AI Agents
AgentPress is a lightweight, powerful utility for kickstarting your LLM App or AI Agent. It provides a simple way to manage message threads, execute LLM calls, and automatically handle tool interactions.
AgentPress is not a agent framework - it's a collection of lightweight, modular utilities that serve as building blocks for creating AI agents. Think of it as "shadcn/ui for AI agents" - a set of utils to copy, paste, and customize in order to quickly bootstrap your AI App / Agent.
## Key Features
AgentPress provides Messages[] API on Steroids called "Threads", a ThreadManager with automatic Tool Execution and a simple StateManager.
- **Thread Management**: Easily create, update, and manage message threads.
- **Automatic Tool Execution**: Define tools as Python classes and have them automatically called by the LLM.
- **Flexible LLM Integration**: Uses LiteLLM under the hood, allowing easy switching between different LLM providers.
- **State Management**: JSON-based state persistence for storing information, tool data, and runtime state.
## Installation
## Installation & Setup
1. Install the package:
```bash
pip install agentpress
```
2. Initialize AgentPress in your project:
```bash
agentpress init
```
This will create a `agentpress` directory with the core utilities you can customize.
## Key Components
- **Threads**: Simple message thread handling utilities
- **Automatic Tool**: Flexible tool definition and automatic execution
- **State Management**: Basic JSON-based state persistence
- **LLM Integration**: Provider-agnostic LLM calls via LiteLLM
## Quick Start
1. Set up your environment variables (API keys, etc.) in a `.env` file.
2. Create a simple tool:
```python
from agentpress.tool import Tool, ToolResult, tool_schema
2. Create a tool - copy this code directly into your project:
```python
from agentpress.tool import Tool, ToolResult, tool_schema
class CalculatorTool(Tool):
@tool_schema({
"name": "add",
"description": "Add two numbers",
"parameters": {
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"}
},
"required": ["a", "b"]
}
})
async def add(self, a: float, b: float) -> ToolResult:
return self.success_response(f"The sum is {a + b}")
```
class CalculatorTool(Tool):
@tool_schema({
"name": "add",
"description": "Add two numbers",
"parameters": {
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"}
},
"required": ["a", "b"]
}
})
async def add(self, a: float, b: float) -> ToolResult:
return self.success_response(f"The sum is {a + b}")
```
3. Use the ThreadManager to run a conversation:
```python
import asyncio
from agentpress.thread_manager import ThreadManager
3. Use the Thread Manager - customize as needed:
```python
import asyncio
from agentpress.thread_manager import ThreadManager
async def main():
manager = ThreadManager()
manager.add_tool(CalculatorTool)
thread_id = await manager.create_thread()
await manager.add_message(thread_id, {"role": "user", "content": "What's 2 + 2?"})
system_message = {"role": "system", "content": "You are a helpful assistant with calculation abilities."}
response = await manager.run_thread(
thread_id=thread_id,
system_message=system_message,
model_name="gpt-4o",
use_tools=True,
execute_model_tool_calls=True
)
print("Response:", response)
async def main():
manager = ThreadManager()
manager.add_tool(CalculatorTool)
thread_id = await manager.create_thread()
# Add your custom logic here
await manager.add_message(thread_id, {
"role": "user",
"content": "What's 2 + 2?"
})
response = await manager.run_thread(
thread_id=thread_id,
system_message={
"role": "system",
"content": "You are a helpful assistant with calculation abilities."
},
model_name="gpt-4",
use_tools=True,
execute_model_tool_calls=True
)
print("Response:", response)
asyncio.run(main())
```
asyncio.run(main())
```
4. Create an autonomous agent with multiple iterations:
```python
import asyncio
from agentpress.thread_manager import ThreadManager
from tools.files_tool import FilesTool
## Building Your Own Agent
async def run_autonomous_agent(max_iterations=5):
thread_manager = ThreadManager()
thread_id = await thread_manager.create_thread()
thread_manager.add_tool(FilesTool)
Example of a customized autonomous agent:
```python
import asyncio
from agentpress.thread_manager import ThreadManager
from your_custom_tools import CustomTool
system_message = {"role": "system", "content": "You are a helpful assistant that can create, read, update, and delete files."}
async def run_agent(max_iterations=5):
# Create your own manager instance
manager = ThreadManager()
thread_id = await manager.create_thread()
# Add your custom tools
manager.add_tool(CustomTool)
# Define your agent's behavior
system_message = {
"role": "system",
"content": "Your custom system message here"
}
# Implement your control loop
for iteration in range(max_iterations):
response = await manager.run_thread(
thread_id=thread_id,
system_message=system_message,
model_name="your-preferred-model",
# Customize parameters as needed
)
# Add your custom logic here
process_response(response)
for iteration in range(max_iterations):
print(f"Iteration {iteration + 1}/{max_iterations}")
await thread_manager.add_message(thread_id, {"role": "user", "content": "Continue!"})
if __name__ == "__main__":
asyncio.run(run_agent())
```
response = await thread_manager.run_thread(
thread_id=thread_id,
system_message=system_message,
model_name="anthropic/claude-3-5-sonnet-20240620",
temperature=0.7,
max_tokens=4096,
tool_choice="auto",
execute_tools_async=False,
execute_model_tool_calls=True
)
## Development
if __name__ == "__main__":
asyncio.run(run_autonomous_agent())
```
This example demonstrates how to create an autonomous agent that runs for a specified number of iterations. It uses the `FilesTool` to
interact with the file system and showcases how to control the behavior of `run_thread` by adjusting parameters like `temperature`,
`max_tokens`, and `tool_choice`. The agent creates files autonomously.
1. Clone for reference:
```bash
git clone https://github.com/kortix-ai/agentpress
cd agentpress
```
## Development Setup
2. Install dependencies:
```bash
pip install poetry
poetry install
```
If you want to contribute or modify the package:
## Philosophy
1. Clone the repository:
```bash
git clone https://github.com/kortix-ai/agentpress
cd agentpress
```
- **Modular**: Pick and choose what you need. Each component is designed to work independently.
- **Agnostic**: Built on LiteLLM, supporting any LLM provider. Minimal opinions, maximum flexibility.
- **Simplicity**: Clean, readable code that's easy to understand and modify.
- **Plug & Play**: Start with our defaults, then customize to your needs.
- **No Lock-in**: Take full ownership of the code. Copy what you need directly into your codebase.
2. Install Poetry (if not already installed):
```bash
pip install poetry
```
3. Install dependencies:
```bash
poetry install
```
## What Makes AgentPress Different?
Unlike monolithic frameworks that dictate how you should build your AI agents, AgentPress provides the essential building blocks while letting you maintain complete control over your implementation. It's designed for developers who want to:
- Quickly prototype AI agents without committing to a heavy framework
- Maintain full ownership and understanding of their agent's code
- Customize and extend functionality based on specific needs
- Learn best practices for building AI agents through clear, practical examples
## Contributing
We welcome contributions to AgentPress! Please feel free to submit issues, fork the repository and send pull requests!
We welcome contributions! Feel free to:
- Submit issues for bugs or suggestions
- Fork the repository and send pull requests
- Share how you've used AgentPress in your projects
## License
[MIT License](LICENSE)
Built with ❤️ by [Kortix AI Corp](https://www.kortix.ai)
Built with ❤️ by [Kortix AI Corp](https://kortix.ai)

View File

@ -1,17 +0,0 @@
from .llm import make_llm_api_call
from .thread_manager import ThreadManager
from .tool import Tool, ToolResult, tool_schema
from .state_manager import StateManager
from .tool_registry import ToolRegistry
__version__ = "0.1.0"
__all__ = [
'make_llm_api_call',
'ThreadManager',
'Tool',
'ToolResult',
'tool_schema',
'StateManager',
'ToolRegistry'
]

180
agentpress/cli.py Normal file
View File

@ -0,0 +1,180 @@
import os
import shutil
import click
import questionary
from typing import List, Dict
import time
MODULES = {
"llm": {
"required": True,
"files": ["llm.py"],
"description": "Core LLM integration module - Handles API calls to language models like GPT-4, Claude, etc."
},
"thread_manager": {
"required": True,
"files": ["thread_manager.py"],
"description": "Message thread management module - Manages conversation history and message flows"
},
"tool_system": {
"required": True,
"files": [
"tool.py",
"tool_registry.py"
],
"description": "Tool execution system - Enables LLMs to use Python functions as tools"
},
"state_manager": {
"required": False,
"files": ["state_manager.py"],
"description": "State persistence module - Saves and loads conversation state and tool data"
}
}
STARTER_EXAMPLES = {
"example-agent": {
"description": "Web development agent with file and terminal tools",
"files": {
"agent.py": "examples/example-agent/agent.py",
"tools/files_tool.py": "examples/example-agent/tools/files_tool.py",
"tools/terminal_tool.py": "examples/example-agent/tools/terminal_tool.py",
}
}
}
def show_welcome():
"""Display welcome message with ASCII art"""
click.clear()
click.echo("""
Welcome to AgentPress
Your AI Agent Building Blocks
""")
time.sleep(1)
def copy_module_files(src_dir: str, dest_dir: str, files: List[str]):
"""Copy module files from package to destination"""
os.makedirs(dest_dir, exist_ok=True)
with click.progressbar(files, label='Copying files') as file_list:
for file in file_list:
src = os.path.join(src_dir, file)
dst = os.path.join(dest_dir, file)
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copy2(src, dst)
def copy_example_files(src_dir: str, dest_dir: str, files: Dict[str, str]):
"""Copy example files from package to destination"""
for dest_path, src_path in files.items():
src = os.path.join(src_dir, src_path)
dst = os.path.join(dest_dir, dest_path)
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copy2(src, dst)
click.echo(f" ✓ Created {dest_path}")
@click.group()
def cli():
"""AgentPress CLI - Initialize your project with AgentPress modules"""
pass
@cli.command()
def init():
"""Initialize AgentPress modules in your project"""
show_welcome()
# Set components directory name to 'agentpress'
components_dir = "agentpress"
if os.path.exists(components_dir):
if not questionary.confirm(
f"Directory '{components_dir}' already exists. Continue anyway?",
default=False
).ask():
click.echo("Setup cancelled.")
return
# Ask about starter examples
click.echo("\n📚 Starter Examples")
example_choices = [
{
"name": f"{name}: {example['description']}",
"value": name
}
for name, example in STARTER_EXAMPLES.items()
]
example_choices.append({"name": "None - I'll start from scratch", "value": None})
selected_example = questionary.select(
"Would you like to start with an example?",
choices=example_choices
).ask()
# Get package directory
package_dir = os.path.dirname(os.path.abspath(__file__))
# Show all modules status
click.echo("\n🔧 AgentPress Modules Configuration\n")
# Show required modules including state_manager
click.echo("📦 Required Modules (pre-selected):")
required_modules = {name: module for name, module in MODULES.items()
if module["required"] or name == "state_manager"}
for name, module in required_modules.items():
click.echo(f"{click.style(name, fg='green')} - {module['description']}")
# Create selections dict with required modules pre-selected
selections = {name: True for name in required_modules.keys()}
click.echo("\n🚀 Setting up your project...")
time.sleep(0.5)
try:
# Copy selected modules
selected_modules = [name for name, selected in selections.items() if selected]
all_files = []
for module in selected_modules:
all_files.extend(MODULES[module]["files"])
# Create components directory and copy module files
components_dir_path = os.path.abspath(components_dir)
copy_module_files(package_dir, components_dir_path, all_files)
# Copy example if selected
if selected_example:
click.echo(f"\n📝 Creating {selected_example}...")
copy_example_files(
package_dir,
os.getcwd(), # Use current working directory
STARTER_EXAMPLES[selected_example]["files"]
)
# Create workspace directory
os.makedirs(os.path.join(os.getcwd(), "workspace"), exist_ok=True)
click.echo("\n✨ Success! Your AgentPress project is ready.")
click.echo(f"\n📁 Components created in: {click.style(components_dir_path, fg='green')}")
if selected_example:
click.echo(f"📁 Example agent files created in the current directory.")
click.echo("\n🔥 Quick start:")
click.echo("1. Create and activate a virtual environment:")
click.echo(" python -m venv venv")
click.echo(" source venv/bin/activate # On Windows: .\\venv\\Scripts\\activate")
if selected_example:
click.echo(f"\n2. Run the example agent:")
click.echo(" python agent.py")
click.echo("\n📚 Import components in your code:")
click.echo(f" from {components_dir}.llm import make_llm_api_call")
click.echo(f" from {components_dir}.thread_manager import ThreadManager")
except Exception as e:
click.echo(f"\n❌ Error during setup: {str(e)}", err=True)
return
def main():
cli()
if __name__ == '__main__':
main()

View File

@ -5,11 +5,10 @@ from tools.files_tool import FilesTool
from agentpress.state_manager import StateManager
from tools.terminal_tool import TerminalTool
async def run_agent():
async def run_agent(thread_id: str, max_iterations: int = 5):
# Initialize managers and tools
thread_manager = ThreadManager()
state_manager = StateManager("state.json")
thread_id = await thread_manager.create_thread()
state_manager = StateManager()
thread_manager.add_tool(FilesTool)
thread_manager.add_tool(TerminalTool)
@ -18,7 +17,7 @@ async def run_agent():
thread_id,
{
"role": "user",
"content": "Let's create a marketing website."
"content": "Let's create a marketing website for my AI Agent 'Jarvis' using HTML, CSS, Javascript. Use images from pixabay, pexels, and co. Style it cyberpunk style. Make it like Ironmen Jarvis."
}
)
@ -29,14 +28,14 @@ async def run_agent():
# Update files state
files_tool = FilesTool()
await files_tool._init_workspace_state()
terminal_tool = TerminalTool()
await terminal_tool.get_command_history()
async def after_iteration():
# Ask the user for a custom message or use the default
custom_message = input("Enter a message to send (or press Enter to use 'Continue!!!' as message): ")
message_content = custom_message if custom_message else "Continue!!!"
await thread_manager.add_message(thread_id, {
"role": "user",
"content": "Continue developing. "
"content": message_content
})
async def finalizer():
@ -45,7 +44,6 @@ async def run_agent():
await init()
iteration = 0
max_iterations = 1
while iteration < max_iterations:
iteration += 1
@ -81,6 +79,7 @@ async def run_agent():
print(response)
# Call after_iteration without arguments
await after_iteration()
await finalizer()
@ -88,5 +87,8 @@ async def run_agent():
if __name__ == "__main__":
async def main():
await run_agent()
asyncio.run(main())
thread_manager = ThreadManager()
thread_id = await thread_manager.create_thread()
await run_agent(thread_id)
asyncio.run(main())

View File

@ -234,4 +234,4 @@ if __name__ == "__main__":
read_deleted_result = await files_tool.read_file(test_file_path)
print("Read deleted file result:", read_deleted_result)
asyncio.run(test_files_tool())
asyncio.run(test_files_tool())

View File

@ -80,22 +80,3 @@ class TerminalTool(Tool):
finally:
# Always restore original directory
os.chdir(original_dir)
@tool_schema({
"name": "get_command_history",
"description": "Get the history of executed commands",
"parameters": {
"type": "object",
"properties": {
"limit": {"type": "integer", "description": "Number of recent commands to return (optional)"}
}
}
})
async def get_command_history(self, limit: int = None) -> ToolResult:
try:
history = await self.state_manager.get("terminal_history") or []
if limit:
history = history[-limit:]
return self.success_response({"history": history})
except Exception as e:
return self.fail_response(f"Error getting command history: {str(e)}")

View File

@ -6,7 +6,7 @@ import openai
from openai import OpenAIError
import asyncio
import logging
import agentops
# import agentops
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY')
ANTHROPIC_API_KEY = os.environ.get('ANTHROPIC_API_KEY')

43
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
[[package]]
name = "agentops"
@ -1378,6 +1378,20 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
typing = ["typing-extensions"]
xmp = ["defusedxml"]
[[package]]
name = "prompt-toolkit"
version = "3.0.36"
description = "Library for building powerful interactive command lines in Python"
optional = false
python-versions = ">=3.6.2"
files = [
{file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"},
{file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"},
]
[package.dependencies]
wcwidth = "*"
[[package]]
name = "propcache"
version = "0.2.0"
@ -1842,6 +1856,20 @@ files = [
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
]
[[package]]
name = "questionary"
version = "2.0.1"
description = "Python library to build pretty command line user prompts ⭐️"
optional = false
python-versions = ">=3.8"
files = [
{file = "questionary-2.0.1-py3-none-any.whl", hash = "sha256:8ab9a01d0b91b68444dff7f6652c1e754105533f083cbe27597c8110ecc230a2"},
{file = "questionary-2.0.1.tar.gz", hash = "sha256:bcce898bf3dbb446ff62830c86c5c6fb9a22a54146f0f5597d3da43b10d8fc8b"},
]
[package.dependencies]
prompt_toolkit = ">=2.0,<=3.0.36"
[[package]]
name = "referencing"
version = "0.35.1"
@ -2517,6 +2545,17 @@ files = [
[package.extras]
watchmedo = ["PyYAML (>=3.10)"]
[[package]]
name = "wcwidth"
version = "0.2.13"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = "*"
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
]
[[package]]
name = "yarl"
version = "1.16.0"
@ -2635,4 +2674,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "317f54f0ccb3b16a6fd730f5d94939e28b0670e4e145a989defc5a12352fa945"
content-hash = "f7f853ada9da04c7de2dd50759d7c81dd8491d6abcf747a055eb22add02512c9"

View File

@ -1,8 +1,8 @@
[tool.poetry]
name = "agentpress"
version = "0.1.1"
description = "LLM Messages[] API on Steroids called \"Threads\" with easy Tool Execution and State Management"
authors = ["marko-kraemer <markokraemer.mail@gmail.com>"]
version = "0.1.2"
description = "Building blocks for AI Agents"
authors = ["marko-kraemer <mail@markokraemer.com>"]
readme = "README.md"
packages = [
{ include = "agentpress" },
@ -26,6 +26,11 @@ streamlit-quill = "^0.0.3"
python-dotenv = "^1.0.1"
litellm = "^1.44.4"
agentops = "^0.3.10"
click = "^8.1.7"
questionary = "^2.0.1"
[tool.poetry.scripts]
agentpress = "agentpress.cli:main"
[build-system]
requires = ["poetry-core"]

7
state.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": {
"index.html": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Jarvis AI Agent</title>\n <link rel=\"stylesheet\" href=\"styles.css\">\n</head>\n<body>\n <header>\n <h1>Welcome to Jarvis AI Agent</h1>\n <img src=\"https://via.placeholder.com/150\" alt=\"Jarvis Logo\" class=\"logo\">\n </header>\n <section id=\"about\">\n <h2>About Jarvis</h2>\n <img src=\"https://via.placeholder.com/600x300\" alt=\"About Image\" class=\"about-image\">\n <p>Jarvis is your personal AI assistant, inspired by Iron Man's AI companion.</p>\n </section>\n <section id=\"features\">\n <h2>Features</h2>\n <ul>\n <li>Voice Commands</li>\n <li>Smart Home Integration</li>\n <li>24/7 Assistance</li>\n </ul>\n </section>\n <section id=\"contact\">\n <h2>Contact Us</h2>\n <p>Email: contact@jarvisai.com</p>\n </section>\n <script src=\"script.js\"></script>\n</body>\n</html>",
"styles.css": "body {\n font-family: 'Roboto', sans-serif;\n margin: 0;\n padding: 0;\n background: linear-gradient(135deg, #0e0b16, #1c1a29);\n color: #e0e0e0;\n}\n\nheader {\n background-color: #1f1b24;\n padding: 20px;\n text-align: center;\n border-bottom: 2px solid #ff2d55;\n}\n\nh1 {\n color: #ff2d55;\n font-size: 3em;\n text-shadow: 0 0 10px #ff2d55, 0 0 20px #ff2d55;\n animation: neon-glow 1.5s infinite alternate;\n}\n\nh2 {\n color: #ff6f61;\n text-shadow: 0 0 5px #ff6f61;\n font-size: 2em;\n margin-bottom: 10px;\n border-bottom: 1px solid #2e2a33;\n padding-bottom: 10px;\n}\n\nsection {\n padding: 20px;\n margin: 20px;\n border: 2px solid #2e2a33;\n border-radius: 10px;\n background: rgba(255, 255, 255, 0.05);\n transition: background 0.3s ease, transform 0.3s ease;\n box-shadow: 0 0 20px rgba(255, 255, 255, 0.1);\n}\n\nsection:hover {\n background: rgba(255, 255, 255, 0.1);\n transform: scale(1.05);\n box-shadow: 0 0 30px rgba(255, 255, 255, 0.2);\n}\n\nul {\n list-style: none;\n padding: 0;\n}\n\nli {\n margin: 10px 0;\n font-size: 1.2em;\n color: #ff9f1c;\n position: relative;\n padding-left: 20px;\n}\n\nli:before {\n content: '\\2022';\n position: absolute;\n left: 0;\n color: #ff2d55;\n font-size: 1.5em;\n line-height: 1em;\n}\n\n.logo {\n width: 150px;\n height: auto;\n margin-top: 10px;\n border: 2px solid #ff2d55;\n border-radius: 50%;\n transition: transform 0.3s ease;\n}\n\n.logo:hover {\n transform: rotate(360deg);\n}\n\n.about-image {\n width: 100%;\n height: auto;\n margin: 20px 0;\n border: 2px solid #ff6f61;\n box-shadow: 0 0 20px rgba(255, 111, 97, 0.5);\n}\n\n@keyframes neon-glow {\n from {\n text-shadow: 0 0 10px #ff2d55, 0 0 20px #ff2d55, 0 0 30px #ff2d55, 0 0 40px #ff2d55;\n }\n to {\n text-shadow: 0 0 20px #ff2d55, 0 0 30px #ff2d55, 0 0 40px #ff2d55, 0 0 50px #ff2d55;\n }\n}",
"script.js": "document.addEventListener('DOMContentLoaded', function() {\n // Example JavaScript functionality\n console.log('Welcome to Jarvis AI Agent!');\n // Add any interactive features you would like here\n});"
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"messages": [{"role": "user", "content": "Let's create a marketing website for my AI Agent 'Jarvis' using HTML, CSS, Javascript. Use images from pixabay, pexels, and co. Style it cyberpunk style. Make it like Ironmen Jarvis."}]}

View File

@ -1 +0,0 @@