diff --git a/agentstack/cli/__init__.py b/agentstack/cli/__init__.py index 32c08ec3..9a369d0b 100644 --- a/agentstack/cli/__init__.py +++ b/agentstack/cli/__init__.py @@ -1,3 +1,3 @@ -from .cli import init_project_builder, configure_default_model, export_template +from .cli import init_project_builder, configure_default_model, export_template, serve_project from .tools import list_tools, add_tool -from .run import run_project \ No newline at end of file +from .run import run_project diff --git a/agentstack/cli/cli.py b/agentstack/cli/cli.py index 1644f109..e4dc4d4c 100644 --- a/agentstack/cli/cli.py +++ b/agentstack/cli/cli.py @@ -1,3 +1,4 @@ +import importlib from typing import Optional import os import sys @@ -19,17 +20,16 @@ from agentstack.logger import log from agentstack import conf from agentstack.conf import ConfigFile -from agentstack.utils import get_package_path +from agentstack.utils import get_package_path, get_framework, validator_not_empty from agentstack.generation.files import ProjectFile from agentstack import frameworks from agentstack import generation from agentstack import inputs from agentstack.agents import get_all_agents from agentstack.tasks import get_all_tasks -from agentstack.utils import open_json_file, term_color, is_snake_case, get_framework, validator_not_empty from agentstack.proj_templates import TemplateConfig from agentstack.exceptions import ValidationError - +from agentstack.utils import open_json_file, term_color, is_snake_case, verify_agentstack_project PREFERRED_MODELS = [ 'openai/gpt-4o', @@ -526,3 +526,14 @@ def export_template(output_filename: str): except Exception as e: print(term_color(f"Failed to write template to file: {e}", 'red')) sys.exit(1) + + +def serve_project(): + verify_agentstack_project() + + # TODO: only silence output conditionally - maybe a debug or verbose option + os.system("docker stop agentstack-local > /dev/null 2>&1") + os.system("docker rm agentstack-local > /dev/null 2>&1") + with importlib.resources.path('agentstack.deploy', 'Dockerfile') as path: + os.system(f"docker build -t agent-service -f {path} .") + os.system("docker run --name agentstack-local -p 6969:6969 agent-service") \ No newline at end of file diff --git a/agentstack/main.py b/agentstack/main.py index ff512982..8593c7cd 100644 --- a/agentstack/main.py +++ b/agentstack/main.py @@ -1,4 +1,5 @@ import sys +from agentstack.cli import init_project_builder, list_tools, configure_default_model, serve_project import argparse import webbrowser @@ -143,6 +144,9 @@ def main(): update = subparsers.add_parser('update', aliases=['u'], help='Check for updates', parents=[global_parser]) + # 'serve' command + serve_parser = subparsers.add_parser('serve', aliases=['s'], help='Serve your agent') + # Parse known args and store unknown args in extras; some commands use them later on args, extra_args = parser.parse_known_args() @@ -190,6 +194,9 @@ def main(): elif args.command in ["run", "r"]: conf.assert_project() run_project(command=args.function, debug=args.debug, cli_args=extra_args) + elif args.command in ['serve', 's']: + conf.assert_project() + serve_project() elif args.command in ['generate', 'g']: conf.assert_project() if args.generate_command in ['agent', 'a']: diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile new file mode 100644 index 00000000..1795b78c --- /dev/null +++ b/agentstack/serve/Dockerfile @@ -0,0 +1,35 @@ +# Dockerfile +FROM python:3.11-slim + +WORKDIR /app + +# install git - TODO: remove after testing +RUN apt-get update && \ + apt-get install -y git && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Copy requirements first to leverage Docker cache +COPY pyproject.toml . +RUN pip install --no-cache-dir poetry +RUN pip install psutil flask + +RUN #pip install agentstack +RUN pip install git+https://github.com/bboynton97/AgentStack.git@deploy +RUN mkdir src +RUN cp /usr/local/lib/python3.11/site-packages/agentstack/deploy/serve.py ./src + +RUN pip uninstall -y agentstack + +RUN apt-get update && apt-get install -y gcc +RUN POETRY_VIRTUALENVS_CREATE=false +RUN poetry config virtualenvs.create false && poetry install + +# Copy the rest of the application +COPY . . + +# Expose the port the app runs on +EXPOSE 6969 + +# Command to run the application +CMD ["python", "src/serve.py"] \ No newline at end of file diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py new file mode 100644 index 00000000..587bc01e --- /dev/null +++ b/agentstack/serve/serve.py @@ -0,0 +1,83 @@ +# app.py +from dotenv import load_dotenv +load_dotenv(dotenv_path="/app/.env") + +from flask import Flask, request, jsonify +import requests +from typing import Dict, Any +import os +from main import run + +app = Flask(__name__) + + +def call_webhook(webhook_url: str, data: Dict[str, Any]) -> None: + """Send results to the specified webhook URL.""" + try: + response = requests.post(webhook_url, json=data) + response.raise_for_status() + except requests.exceptions.RequestException as e: + app.logger.error(f"Webhook call failed: {str(e)}") + raise + + +@app.route("/health", methods=["GET"]) +def health(): + return "Agent Server Up" + + +@app.route('/process', methods=['POST']) +def process_agent(): + try: + # Extract data and webhook URL from request + request_data = request.get_json() + if not request_data or 'webhook_url' not in request_data: + return jsonify({'error': 'Missing webhook_url in request'}), 400 + + webhook_url = request_data.pop('webhook_url') + + # Run the agent process with the provided data + # result = WebresearcherCrew().crew().kickoff(inputs=request_data) + # inputs = json.stringify(request_data) + # os.system(f"python src/main.py {inputs}") + result = run(request_data) + + # Call the webhook with the results + call_webhook(webhook_url, { + 'status': 'success', + 'result': result + }) + + return jsonify({ + 'status': 'success', + 'message': 'Agent process completed and webhook called' + }) + + except Exception as e: + error_message = str(e) + app.logger.error(f"Error processing request: {error_message}") + + # Attempt to call webhook with error information + if webhook_url: + try: + call_webhook(webhook_url, { + 'status': 'error', + 'error': error_message + }) + except: + pass # Webhook call failed, but we still want to return the error to the caller + + return jsonify({ + 'status': 'error', + 'error': error_message + }), 500 + + +if __name__ == '__main__': + port = int(os.environ.get('PORT', 6969)) + + print("🚧 Running your agent on a development server") + print(f"Send agent requests to http://localhost:{port}") + print("Learn more about agent requests at https://docs.agentstack.sh/") # TODO: add docs for this + + app.run(host='0.0.0.0', port=port)