From d8e7afacc5a839fe210c3e936546c6f92d8961d2 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 19 Nov 2024 13:36:23 -0800 Subject: [PATCH 01/40] serve --- agentstack/cli/__init__.py | 2 +- agentstack/cli/cli.py | 12 +++++- agentstack/deploy/Dockerfile | 36 ++++++++++++++++++ agentstack/deploy/serve.py | 73 ++++++++++++++++++++++++++++++++++++ agentstack/main.py | 7 +++- agentstack/utils.py | 1 - 6 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 agentstack/deploy/Dockerfile create mode 100644 agentstack/deploy/serve.py diff --git a/agentstack/cli/__init__.py b/agentstack/cli/__init__.py index 3c35ec37..8a40e493 100644 --- a/agentstack/cli/__init__.py +++ b/agentstack/cli/__init__.py @@ -1 +1 @@ -from .cli import init_project_builder, list_tools +from .cli import init_project_builder, list_tools, serve_project diff --git a/agentstack/cli/cli.py b/agentstack/cli/cli.py index 7277db32..30985f60 100644 --- a/agentstack/cli/cli.py +++ b/agentstack/cli/cli.py @@ -13,7 +13,7 @@ from .agentstack_data import FrameworkData, ProjectMetadata, ProjectStructure, CookiecutterData from agentstack.logger import log from .. import generation -from ..utils import open_json_file, term_color, is_snake_case +from ..utils import open_json_file, term_color, is_snake_case, verify_agentstack_project def init_project_builder(slug_name: Optional[str] = None, skip_wizard: bool = False): @@ -344,4 +344,12 @@ def list_tools(): except json.JSONDecodeError: print("Error: tools.json contains invalid JSON.") except Exception as e: - print(f"An unexpected error occurred: {e}") \ No newline at end of file + print(f"An unexpected error occurred: {e}") + + +def serve_project(): + verify_agentstack_project() + + 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") diff --git a/agentstack/deploy/Dockerfile b/agentstack/deploy/Dockerfile new file mode 100644 index 00000000..356122e5 --- /dev/null +++ b/agentstack/deploy/Dockerfile @@ -0,0 +1,36 @@ +# 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 + +RUN #pip install agentstack +RUN pip install git+https://github.com/bboynton97/AgentStack.git +#RUN ls /usr/local/lib/python3.11/site-packages +#RUN ls /usr/local/lib/python3.11/site-packages/agentstack +#RUN ls /usr/local/lib/python3.11/site-packages/agentstack/deploy +#COPY /usr/local/lib/python3.11/site-packages/agentstack/deploy/serve.py . + +RUN #pip uninstall -y agentstack + +#RUN 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", "serve.py"] +CMD ["sleep", "infinity"] \ No newline at end of file diff --git a/agentstack/deploy/serve.py b/agentstack/deploy/serve.py new file mode 100644 index 00000000..145ebb8b --- /dev/null +++ b/agentstack/deploy/serve.py @@ -0,0 +1,73 @@ +# app.py +import json + +from flask import Flask, request, jsonify +import requests +from agent_script import run_agent # Your existing agent script +from typing import Dict, Any +import os +from .src.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('/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)) + app.run(host='0.0.0.0', port=port) \ No newline at end of file diff --git a/agentstack/main.py b/agentstack/main.py index fdc61804..2ac948ed 100644 --- a/agentstack/main.py +++ b/agentstack/main.py @@ -1,7 +1,7 @@ import argparse import sys -from agentstack.cli import init_project_builder, list_tools +from agentstack.cli import init_project_builder, list_tools, serve_project from agentstack.utils import get_version import agentstack.generation as generation @@ -66,6 +66,9 @@ def main(): tools_remove_parser = tools_subparsers.add_parser('remove', aliases=['r'], help='Remove a tool') tools_remove_parser.add_argument('name', help='Name of the tool to remove') + # 'deploy' command + serve_parser = subparsers.add_parser('serve', aliases=['s'], help='Serve your agent') + # Parse arguments args = parser.parse_args() @@ -97,6 +100,8 @@ def main(): generation.remove_tool(args.name) else: tools_parser.print_help() + if args.command in ['serve', 's']: + serve_project() else: parser.print_help() diff --git a/agentstack/utils.py b/agentstack/utils.py index 822208d5..2f2a929a 100644 --- a/agentstack/utils.py +++ b/agentstack/utils.py @@ -78,7 +78,6 @@ def term_color(text: str, color: str) -> str: return text - def is_snake_case(string: str): return bool(re.match('^[a-z0-9_]+$', string)) From 7a60fc01b512db5d891fabadea9410a5e5dc4c59 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 19 Nov 2024 15:37:56 -0800 Subject: [PATCH 02/40] fix serve file and dockerfile --- agentstack/cli/cli.py | 3 +++ agentstack/deploy/Dockerfile | 18 ++++++++++-------- agentstack/deploy/serve.py | 5 ++--- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/agentstack/cli/cli.py b/agentstack/cli/cli.py index 30985f60..376a97f0 100644 --- a/agentstack/cli/cli.py +++ b/agentstack/cli/cli.py @@ -350,6 +350,9 @@ def list_tools(): 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") diff --git a/agentstack/deploy/Dockerfile b/agentstack/deploy/Dockerfile index 356122e5..ddff1daf 100644 --- a/agentstack/deploy/Dockerfile +++ b/agentstack/deploy/Dockerfile @@ -15,15 +15,15 @@ RUN pip install --no-cache-dir poetry RUN pip install psutil RUN #pip install agentstack -RUN pip install git+https://github.com/bboynton97/AgentStack.git -#RUN ls /usr/local/lib/python3.11/site-packages -#RUN ls /usr/local/lib/python3.11/site-packages/agentstack -#RUN ls /usr/local/lib/python3.11/site-packages/agentstack/deploy -#COPY /usr/local/lib/python3.11/site-packages/agentstack/deploy/serve.py . +RUN pip install git+https://github.com/bboynton97/AgentStack.git@deploy +RUN cp /usr/local/lib/python3.11/site-packages/agentstack/deploy/serve.py ./src -RUN #pip uninstall -y agentstack +RUN pip uninstall -y agentstack -#RUN poetry install +RUN apt-get update && apt-get install -y gcc +RUN POETRY_VIRTUALENVS_CREATE=false +RUN poetry config virtualenvs.create false && poetry install +RUN pip install flask # Copy the rest of the application COPY . . @@ -31,6 +31,8 @@ COPY . . # Expose the port the app runs on EXPOSE 6969 +WORKDIR . + # Command to run the application -#CMD ["python", "serve.py"] +#CMD ["python", "src/serve.py"] CMD ["sleep", "infinity"] \ No newline at end of file diff --git a/agentstack/deploy/serve.py b/agentstack/deploy/serve.py index 145ebb8b..d48a811f 100644 --- a/agentstack/deploy/serve.py +++ b/agentstack/deploy/serve.py @@ -3,10 +3,9 @@ from flask import Flask, request, jsonify import requests -from agent_script import run_agent # Your existing agent script from typing import Dict, Any import os -from .src.main import run +from src.main import run app = Flask(__name__) @@ -70,4 +69,4 @@ def process_agent(): if __name__ == '__main__': port = int(os.environ.get('PORT', 6969)) - app.run(host='0.0.0.0', port=port) \ No newline at end of file + app.run(host='0.0.0.0', port=port) From 7f88cc034b3655fda6ce9f0de630c73dd1c449a5 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 19 Nov 2024 15:52:00 -0800 Subject: [PATCH 03/40] serve from inside source --- agentstack/deploy/Dockerfile | 10 ++++------ agentstack/deploy/serve.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/agentstack/deploy/Dockerfile b/agentstack/deploy/Dockerfile index ddff1daf..de9cf72c 100644 --- a/agentstack/deploy/Dockerfile +++ b/agentstack/deploy/Dockerfile @@ -12,10 +12,11 @@ RUN apt-get update && \ # Copy requirements first to leverage Docker cache COPY pyproject.toml . RUN pip install --no-cache-dir poetry -RUN pip install psutil +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 @@ -23,7 +24,6 @@ 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 -RUN pip install flask # Copy the rest of the application COPY . . @@ -31,8 +31,6 @@ COPY . . # Expose the port the app runs on EXPOSE 6969 -WORKDIR . - # Command to run the application -#CMD ["python", "src/serve.py"] -CMD ["sleep", "infinity"] \ No newline at end of file +CMD ["python", "src/serve.py"] +#CMD ["sleep", "infinity"] \ No newline at end of file diff --git a/agentstack/deploy/serve.py b/agentstack/deploy/serve.py index d48a811f..d40a3c85 100644 --- a/agentstack/deploy/serve.py +++ b/agentstack/deploy/serve.py @@ -5,7 +5,7 @@ import requests from typing import Dict, Any import os -from src.main import run +from main import run app = Flask(__name__) From b06783713c367ea359f530dbe19299f911b3df90 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 19 Nov 2024 15:57:58 -0800 Subject: [PATCH 04/40] load dotenv --- agentstack/deploy/serve.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agentstack/deploy/serve.py b/agentstack/deploy/serve.py index d40a3c85..990a9600 100644 --- a/agentstack/deploy/serve.py +++ b/agentstack/deploy/serve.py @@ -1,11 +1,11 @@ # app.py -import json - from flask import Flask, request, jsonify import requests from typing import Dict, Any import os from main import run +from dotenv import load_dotenv +load_dotenv() app = Flask(__name__) From 606fd44117d4f653cc2fa6ac75966ef792702aab Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 19 Nov 2024 16:14:25 -0800 Subject: [PATCH 05/40] load dotenv from one up --- agentstack/deploy/Dockerfile | 6 ++++-- agentstack/deploy/serve.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/agentstack/deploy/Dockerfile b/agentstack/deploy/Dockerfile index de9cf72c..43c08fae 100644 --- a/agentstack/deploy/Dockerfile +++ b/agentstack/deploy/Dockerfile @@ -9,6 +9,8 @@ RUN apt-get update && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* +RUN echo "hi mom" + # Copy requirements first to leverage Docker cache COPY pyproject.toml . RUN pip install --no-cache-dir poetry @@ -32,5 +34,5 @@ COPY . . EXPOSE 6969 # Command to run the application -CMD ["python", "src/serve.py"] -#CMD ["sleep", "infinity"] \ No newline at end of file +#CMD ["python", "src/serve.py"] +CMD ["sleep", "infinity"] \ No newline at end of file diff --git a/agentstack/deploy/serve.py b/agentstack/deploy/serve.py index 990a9600..67d27711 100644 --- a/agentstack/deploy/serve.py +++ b/agentstack/deploy/serve.py @@ -5,7 +5,7 @@ import os from main import run from dotenv import load_dotenv -load_dotenv() +load_dotenv(dotenv_path="../") app = Flask(__name__) From a240d5dba6db010d516040d64cc5969f055003d3 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 19 Nov 2024 16:26:04 -0800 Subject: [PATCH 06/40] health endpoint --- agentstack/deploy/Dockerfile | 6 +++--- agentstack/deploy/serve.py | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/agentstack/deploy/Dockerfile b/agentstack/deploy/Dockerfile index 43c08fae..8406ce3f 100644 --- a/agentstack/deploy/Dockerfile +++ b/agentstack/deploy/Dockerfile @@ -9,7 +9,7 @@ RUN apt-get update && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* -RUN echo "hi mom" +RUN echo "hi dad" # Copy requirements first to leverage Docker cache COPY pyproject.toml . @@ -34,5 +34,5 @@ COPY . . EXPOSE 6969 # Command to run the application -#CMD ["python", "src/serve.py"] -CMD ["sleep", "infinity"] \ No newline at end of file +CMD ["python", "src/serve.py"] +#CMD ["sleep", "infinity"] \ No newline at end of file diff --git a/agentstack/deploy/serve.py b/agentstack/deploy/serve.py index 67d27711..d76c0e15 100644 --- a/agentstack/deploy/serve.py +++ b/agentstack/deploy/serve.py @@ -20,6 +20,11 @@ def call_webhook(webhook_url: str, data: Dict[str, Any]) -> None: raise +@app.route("/health", methods=["GET"]) +def health(): + return "Agent Server Up" + + @app.route('/process', methods=['POST']) def process_agent(): try: From b9d994214a83ed5f4ce1c48e1bb3f530564519ba Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 19 Nov 2024 16:27:57 -0800 Subject: [PATCH 07/40] env path --- agentstack/deploy/serve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agentstack/deploy/serve.py b/agentstack/deploy/serve.py index d76c0e15..8e264d07 100644 --- a/agentstack/deploy/serve.py +++ b/agentstack/deploy/serve.py @@ -5,7 +5,7 @@ import os from main import run from dotenv import load_dotenv -load_dotenv(dotenv_path="../") +load_dotenv(dotenv_path="../.env") app = Flask(__name__) From e25b51a2f77d0b1418f12bd02973bfa90eb8aa1c Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 19 Nov 2024 16:48:17 -0800 Subject: [PATCH 08/40] loadenv before import --- agentstack/deploy/Dockerfile | 5 +---- agentstack/deploy/serve.py | 5 +++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/agentstack/deploy/Dockerfile b/agentstack/deploy/Dockerfile index 8406ce3f..1795b78c 100644 --- a/agentstack/deploy/Dockerfile +++ b/agentstack/deploy/Dockerfile @@ -9,8 +9,6 @@ RUN apt-get update && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* -RUN echo "hi dad" - # Copy requirements first to leverage Docker cache COPY pyproject.toml . RUN pip install --no-cache-dir poetry @@ -34,5 +32,4 @@ COPY . . EXPOSE 6969 # Command to run the application -CMD ["python", "src/serve.py"] -#CMD ["sleep", "infinity"] \ No newline at end of file +CMD ["python", "src/serve.py"] \ No newline at end of file diff --git a/agentstack/deploy/serve.py b/agentstack/deploy/serve.py index 8e264d07..ac0c32d3 100644 --- a/agentstack/deploy/serve.py +++ b/agentstack/deploy/serve.py @@ -1,11 +1,12 @@ # 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 -from dotenv import load_dotenv -load_dotenv(dotenv_path="../.env") app = Flask(__name__) From 6896a22d70353c8fe18ad5919c823a1c9ddf791e Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Sun, 22 Dec 2024 23:49:19 -0500 Subject: [PATCH 09/40] create project on deploy --- agentstack/conf.py | 6 ++ agentstack/deploy.py | 61 +++++++++++++++++++ agentstack/main.py | 8 +++ .../agentstack.json | 1 + 4 files changed, 76 insertions(+) create mode 100644 agentstack/deploy.py diff --git a/agentstack/conf.py b/agentstack/conf.py index 2b7810e4..5ca41bf7 100644 --- a/agentstack/conf.py +++ b/agentstack/conf.py @@ -51,6 +51,8 @@ class ConfigFile(BaseModel): Config Schema ------------- + project_name: str + The name of the project. framework: str The framework used in the project. Defaults to 'crewai'. tools: list[str] @@ -65,8 +67,11 @@ class ConfigFile(BaseModel): The template used to generate the project. template_version: Optional[str] The version of the template system used to generate the project. + hosted_project_id: Optional[str] + The ID of the deployed project on https://AgentStack.sh """ + project_name: str framework: str = DEFAULT_FRAMEWORK # TODO this should probably default to None tools: list[str] = [] telemetry_opt_out: Optional[bool] = None @@ -74,6 +79,7 @@ class ConfigFile(BaseModel): agentstack_version: Optional[str] = get_version() template: Optional[str] = None template_version: Optional[str] = None + hosted_project_id: Optional[int] = None def __init__(self): if os.path.exists(PATH / CONFIG_FILENAME): diff --git a/agentstack/deploy.py b/agentstack/deploy.py new file mode 100644 index 00000000..988af520 --- /dev/null +++ b/agentstack/deploy.py @@ -0,0 +1,61 @@ +import webbrowser + +from agentstack.auth import get_stored_token, login +from agentstack.conf import ConfigFile +from agentstack.utils import term_color +import requests + + +def deploy(): + bearer_token = get_stored_token() + if not bearer_token: + success = login() + if success: + bearer_token = get_stored_token() + else: + print(term_color("Failed to authenticate with AgentStack.sh", "red")) + return + + project_id = get_project_id() + webbrowser.open(f"http://localhost:5173/project/{project_id}") + + +def get_project_id(): + project_config = ConfigFile() + project_id = project_config.hosted_project_id + + if project_id: + return project_id + + bearer_token = get_stored_token() + + # if not in config, create project and store it + print(term_color("š§ Creating AgentStack.sh Project", "green")) + headers = { + 'Authorization': f'Bearer {bearer_token}', + 'Content-Type': 'application/json' + } + + payload = { + 'name': project_config.project_name + } + + try: + response = requests.post( + url="http://localhost:3000/projects", + # url="https://api.agentstack.sh/projects", + headers=headers, + json=payload + ) + + response.raise_for_status() + res_data = response.json() + project_id = res_data['id'] + project_config.hosted_project_id = project_id + project_config.write() + return project_id + + except requests.exceptions.RequestException as e: + print(f"Error making request: {e}") + return None + diff --git a/agentstack/main.py b/agentstack/main.py index 07e0c975..47a65a66 100644 --- a/agentstack/main.py +++ b/agentstack/main.py @@ -15,6 +15,7 @@ from agentstack.utils import get_version, term_color from agentstack import generation from agentstack.update import check_for_updates +from agentstack.deploy import deploy def main(): @@ -136,13 +137,18 @@ def main(): ) tools_remove_parser.add_argument("name", help="Name of the tool to remove") + # 'export' export_parser = subparsers.add_parser( 'export', aliases=['e'], help='Export your agent as a template', parents=[global_parser] ) export_parser.add_argument('filename', help='The name of the file to export to') + # 'update' update = subparsers.add_parser('update', aliases=['u'], help='Check for updates', parents=[global_parser]) + # 'deploy' + deploy_ = subparsers.add_parser('deploy', aliases=['d'], help='Deploy your agent to AgentStack.sh', parents=[global_parser]) + # Parse known args and store unknown args in extras; some commands use them later on args, extra_args = parser.parse_known_args() @@ -193,6 +199,8 @@ def main(): export_template(args.filename) elif args.command in ['login']: auth.login() + elif args.command in ['deploy', 'd']: + deploy() elif args.command in ['update', 'u']: pass # Update check already done else: diff --git a/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/agentstack.json b/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/agentstack.json index 5511a17a..3243f1a6 100644 --- a/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/agentstack.json +++ b/agentstack/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/agentstack.json @@ -1,4 +1,5 @@ { + "project_name": "{{ cookiecutter.project_metadata.project_name }}", "framework": "{{ cookiecutter.framework }}", "agentstack_version": "{{ cookiecutter.project_metadata.agentstack_version }}", "template": "{{ cookiecutter.project_metadata.template }}", From 41b9b9a2cb1804f7d05f8a7ee89e223b73c36c47 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Mon, 30 Dec 2024 19:12:03 +0000 Subject: [PATCH 10/40] zip code and upload --- agentstack/deploy.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/agentstack/deploy.py b/agentstack/deploy.py index 988af520..88bf9234 100644 --- a/agentstack/deploy.py +++ b/agentstack/deploy.py @@ -1,4 +1,9 @@ +import os +import tempfile +import tomllib import webbrowser +import zipfile +from pathlib import Path from agentstack.auth import get_stored_token, login from agentstack.conf import ConfigFile @@ -17,9 +22,37 @@ def deploy(): return project_id = get_project_id() + pyproject = load_pyproject() + files = list(Path('.').rglob('*.py')) + + with tempfile.NamedTemporaryFile(suffix='.zip') as tmp: + with zipfile.ZipFile(tmp.name, 'w') as zf: + for file in files: + zf.write(file) + if pyproject: + zf.write("pyproject.toml") + + response = requests.post( + 'http://localhost:3000/deploy/build', + files={'code': ('code.zip', open(tmp.name, 'rb'))}, + params={'projectId': project_id}, + headers={'Authorization': bearer_token} + ) + + if response.status_code != 200: + print(term_color("Failed to deploy with AgentStack.sh", "red")) + print(response.text) + return + webbrowser.open(f"http://localhost:5173/project/{project_id}") +def load_pyproject(): + if os.path.exists("pyproject.toml"): + with open("pyproject.toml", "rb") as f: + return tomllib.load(f) + return None + def get_project_id(): project_config = ConfigFile() project_id = project_config.hosted_project_id From dcacacab417146ffa189fa2e0028fd2e3b498efb Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 7 Jan 2025 20:25:21 +0000 Subject: [PATCH 11/40] fix deploy --- agentstack/deploy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/agentstack/deploy.py b/agentstack/deploy.py index 88bf9234..9b8fea63 100644 --- a/agentstack/deploy.py +++ b/agentstack/deploy.py @@ -36,7 +36,7 @@ def deploy(): 'http://localhost:3000/deploy/build', files={'code': ('code.zip', open(tmp.name, 'rb'))}, params={'projectId': project_id}, - headers={'Authorization': bearer_token} + headers={'Authorization': f'Bearer {bearer_token}'} ) if response.status_code != 200: @@ -44,6 +44,7 @@ def deploy(): print(response.text) return + print(term_color("š Successfully deployed with AgentStack.sh! Opening in browser...", "green")) webbrowser.open(f"http://localhost:5173/project/{project_id}") From ef3a3fd52a1fac30ec8a737e2810968b78b3d8f3 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 7 Jan 2025 20:38:17 +0000 Subject: [PATCH 12/40] serve working --- agentstack/cli/__init__.py | 2 +- agentstack/cli/cli.py | 6 +++--- agentstack/main.py | 4 ---- agentstack/{deploy => serve}/Dockerfile | 0 agentstack/{deploy => serve}/serve.py | 5 +++++ 5 files changed, 9 insertions(+), 8 deletions(-) rename agentstack/{deploy => serve}/Dockerfile (100%) rename agentstack/{deploy => serve}/serve.py (91%) diff --git a/agentstack/cli/__init__.py b/agentstack/cli/__init__.py index 481991b0..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, list_tools +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 diff --git a/agentstack/cli/cli.py b/agentstack/cli/cli.py index cc909e36..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,16 +20,15 @@ from agentstack.logger import log from agentstack import conf from agentstack.conf import ConfigFile -from agentstack.utils import get_package_path, open_json_file, term_color, is_snake_case, get_framework, validator_not_empty +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 packaging, generation +from agentstack import generation from agentstack import inputs from agentstack.agents import get_all_agents from agentstack.tasks import get_all_tasks from agentstack.proj_templates import TemplateConfig from agentstack.exceptions import ValidationError -from agentstack.generation.files import ConfigFile from agentstack.utils import open_json_file, term_color, is_snake_case, verify_agentstack_project PREFERRED_MODELS = [ diff --git a/agentstack/main.py b/agentstack/main.py index 9148bf0e..8593c7cd 100644 --- a/agentstack/main.py +++ b/agentstack/main.py @@ -1,9 +1,5 @@ import sys from agentstack.cli import init_project_builder, list_tools, configure_default_model, serve_project -from agentstack.telemetry import track_cli_command -from agentstack.utils import get_version, get_framework -import agentstack.generation as generation -from agentstack.update import check_for_updates import argparse import webbrowser diff --git a/agentstack/deploy/Dockerfile b/agentstack/serve/Dockerfile similarity index 100% rename from agentstack/deploy/Dockerfile rename to agentstack/serve/Dockerfile diff --git a/agentstack/deploy/serve.py b/agentstack/serve/serve.py similarity index 91% rename from agentstack/deploy/serve.py rename to agentstack/serve/serve.py index ac0c32d3..587bc01e 100644 --- a/agentstack/deploy/serve.py +++ b/agentstack/serve/serve.py @@ -75,4 +75,9 @@ def process_agent(): 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) From 71f474ffffaf21a754f2923944855116c48b597e Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Mon, 20 Jan 2025 14:46:46 -0800 Subject: [PATCH 13/40] build all but some folders --- agentstack/deploy.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/agentstack/deploy.py b/agentstack/deploy.py index 9b8fea63..4e8e32d1 100644 --- a/agentstack/deploy.py +++ b/agentstack/deploy.py @@ -23,7 +23,19 @@ def deploy(): project_id = get_project_id() pyproject = load_pyproject() - files = list(Path('.').rglob('*.py')) + + def should_skip_dir(path: Path) -> bool: + skip_dirs = {'.venv', '__pycache__', '.git', 'build', 'dist'} + return path.name in skip_dirs + + files = [] + for path in Path('.').iterdir(): + if path.is_dir(): + if should_skip_dir(path): + continue + files.extend(p for p in path.rglob('*.py')) + elif path.suffix == '.py': + files.append(path) with tempfile.NamedTemporaryFile(suffix='.zip') as tmp: with zipfile.ZipFile(tmp.name, 'w') as zf: From 620f7fbd896e47706ebc7bc3873d16e4950285b8 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 21 Jan 2025 23:11:56 -0800 Subject: [PATCH 14/40] better upload --- agentstack/cli/spinner.py | 81 +++++++++++++++++++ agentstack/deploy.py | 165 +++++++++++++++++++++++++++++--------- 2 files changed, 208 insertions(+), 38 deletions(-) create mode 100644 agentstack/cli/spinner.py diff --git a/agentstack/cli/spinner.py b/agentstack/cli/spinner.py new file mode 100644 index 00000000..7fdcad71 --- /dev/null +++ b/agentstack/cli/spinner.py @@ -0,0 +1,81 @@ +import itertools +import shutil +import sys +import threading +import time +from agentstack import log + + +class Spinner: + def __init__(self, message="Working", delay=0.1): + self.spinner = itertools.cycle(['ā ', 'ā ', 'ā ¹', 'ā ø', 'ā ¼', 'ā “', 'ā ¦', 'ā §', 'ā ', 'ā ']) + self.delay = delay + self.message = message + self.running = False + self.spinner_thread = None + self.start_time = None + self._lock = threading.Lock() + self._last_printed_len = 0 + + def _clear_line(self): + """Clear the current line in terminal.""" + sys.stdout.write('\r' + ' ' * self._last_printed_len + '\r') + sys.stdout.flush() + + def spin(self): + while self.running: + with self._lock: + elapsed = time.time() - self.start_time + terminal_width = shutil.get_terminal_size().columns + spinner_char = next(self.spinner) + time_str = f"{elapsed:.1f}s" + + # Format: [spinner] Message... [time] + message = f"\r{spinner_char} {self.message}... [{time_str}]" + + # Ensure we don't exceed terminal width + if len(message) > terminal_width: + message = message[:terminal_width - 3] + "..." + + # Clear previous line and print new one + self._clear_line() + sys.stdout.write(message) + sys.stdout.flush() + self._last_printed_len = len(message) + + time.sleep(self.delay) + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop() + + def start(self): + if not self.running: + self.running = True + self.start_time = time.time() + self.spinner_thread = threading.Thread(target=self.spin) + self.spinner_thread.start() + + def stop(self): + if self.running: + self.running = False + if self.spinner_thread: + self.spinner_thread.join() + with self._lock: + self._clear_line() + + def update_message(self, message): + """Update spinner message and ensure clean line.""" + with self._lock: + self._clear_line() + self.message = message + + def clear_and_log(self, message): + """Temporarily clear spinner, print message, and resume spinner.""" + with self._lock: + self._clear_line() + log.success(message) + sys.stdout.flush() \ No newline at end of file diff --git a/agentstack/deploy.py b/agentstack/deploy.py index 4e8e32d1..42100d31 100644 --- a/agentstack/deploy.py +++ b/agentstack/deploy.py @@ -1,64 +1,82 @@ import os +import sys import tempfile +import time import tomllib import webbrowser import zipfile from pathlib import Path from agentstack.auth import get_stored_token, login +from agentstack.cli.spinner import Spinner from agentstack.conf import ConfigFile from agentstack.utils import term_color +from agentstack import log import requests def deploy(): + log.info("Deploying your agentstack agent!") bearer_token = get_stored_token() if not bearer_token: success = login() if success: bearer_token = get_stored_token() else: - print(term_color("Failed to authenticate with AgentStack.sh", "red")) + log.error(term_color("Failed to authenticate with AgentStack.sh", "red")) return project_id = get_project_id() pyproject = load_pyproject() - def should_skip_dir(path: Path) -> bool: - skip_dirs = {'.venv', '__pycache__', '.git', 'build', 'dist'} - return path.name in skip_dirs - - files = [] - for path in Path('.').iterdir(): - if path.is_dir(): - if should_skip_dir(path): - continue - files.extend(p for p in path.rglob('*.py')) - elif path.suffix == '.py': - files.append(path) - - with tempfile.NamedTemporaryFile(suffix='.zip') as tmp: - with zipfile.ZipFile(tmp.name, 'w') as zf: - for file in files: - zf.write(file) - if pyproject: - zf.write("pyproject.toml") - - response = requests.post( - 'http://localhost:3000/deploy/build', - files={'code': ('code.zip', open(tmp.name, 'rb'))}, - params={'projectId': project_id}, - headers={'Authorization': f'Bearer {bearer_token}'} - ) - - if response.status_code != 200: - print(term_color("Failed to deploy with AgentStack.sh", "red")) - print(response.text) - return - - print(term_color("š Successfully deployed with AgentStack.sh! Opening in browser...", "green")) - webbrowser.open(f"http://localhost:5173/project/{project_id}") - + # def should_skip_dir(path: Path) -> bool: + # skip_dirs = {'.venv', '__pycache__', '.git', 'build', 'dist'} + # return path.name in skip_dirs + + # files = list(Path('.').rglob('*.py')) + # with tempfile.NamedTemporaryFile(suffix='.zip') as tmp: + # with zipfile.ZipFile(tmp.name, 'w') as zf: + # for file in files: + # zf.write(file) + # if pyproject: + # zf.write("pyproject.toml") + + zip_file = None + + with Spinner() as spinner: + time.sleep(0.1) + try: + spinner.update_message("Collecting files") + spinner.clear_and_log(" šļø Files collected") + files = collect_files(str(Path('.')), ('.py', '.toml', '.yaml', '.json')) + if not files: + raise Exception("No files found to deploy") + + spinner.update_message("Creating zip file") + zip_file = create_zip_in_memory(files, spinner) + spinner.clear_and_log(" šļø Created zip file") + + spinner.update_message("Uploading to server") + + response = requests.post( + 'http://localhost:3000/deploy/build', + files={'code': ('code.zip', zip_file)}, + params={'projectId': project_id}, + headers={'Authorization': f'Bearer {bearer_token}'} + ) + + spinner.clear_and_log(" š” Uploaded to server") + + if response.status_code != 200: + raise Exception(response.text) + + spinner.clear_and_log("š Successfully deployed with AgentStack.sh! Opening in browser...") + webbrowser.open(f"http://localhost:5173/project/{project_id}") + + except Exception as e: + spinner.stop() + log.error(f"š Failed to deploy with AgentStack.sh: {e}") + return def load_pyproject(): if os.path.exists("pyproject.toml"): @@ -76,7 +94,7 @@ def get_project_id(): bearer_token = get_stored_token() # if not in config, create project and store it - print(term_color("š§ Creating AgentStack.sh Project", "green")) + log.info("š§ Creating AgentStack.sh Project") headers = { 'Authorization': f'Bearer {bearer_token}', 'Content-Type': 'application/json' @@ -102,6 +120,77 @@ def get_project_id(): return project_id except requests.exceptions.RequestException as e: - print(f"Error making request: {e}") + log.error(f"Error making request: {e}") return None + +def collect_files(root_path='.', file_types=('.py', '.toml', '.yaml', '.json')): + """Collect files of specified types from directory tree.""" + files = set() # Using set for faster lookups and unique entries + root = Path(root_path) + + def should_process_dir(path): + """Check if directory should be processed.""" + skip_dirs = {'.git', '.venv', 'venv', '__pycache__', 'node_modules', '.pytest_cache'} + return path.name not in skip_dirs + + def process_directory(path): + """Process a directory and collect matching files.""" + if not should_process_dir(path): + return set() + + matching_files = set() + try: + for file_path in path.iterdir(): + if file_path.is_file() and file_path.suffix in file_types: + matching_files.add(file_path) + elif file_path.is_dir(): + matching_files.update(process_directory(file_path)) + except PermissionError: + log.error(f"Permission denied accessing {path}") + except Exception as e: + log.error(f"Error processing {path}: {e}") + + return matching_files + + # Start with files in root directory + files.update(f for f in root.iterdir() if f.is_file() and f.suffix in file_types) + + # Process subdirectories + for path in root.iterdir(): + if path.is_dir(): + files.update(process_directory(path)) + + return sorted(files) # Convert back to sorted list for consistent ordering + + +def create_zip_in_memory(files, spinner): + """Create a ZIP file in memory with progress updates.""" + tmp = tempfile.SpooledTemporaryFile(max_size=10 * 1024 * 1024) + total_files = len(files) + + with zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED) as zf: + for i, file in enumerate(files, 1): + try: + spinner.update_message(f"Adding files to zip ({i}/{total_files})") + zf.write(file) + except Exception as e: + log.error(f"Error adding {file} to zip: {e}") + + tmp.seek(0) + + # Get final zip size + current_pos = tmp.tell() + tmp.seek(0, 2) # Seek to end + zip_size = tmp.tell() + tmp.seek(current_pos) # Restore position + + def format_size(size_bytes): + for unit in ['B', 'KB', 'MB', 'GB']: + if size_bytes < 1024 or unit == 'GB': + return f"{size_bytes:.1f}{unit}" + size_bytes /= 1024 + + # log.info(f" > Zip created: {format_size(zip_size)}") + + return tmp \ No newline at end of file From d8055bd7846de15b66eb696b12e423a5d5a33366 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 21 Jan 2025 23:37:56 -0800 Subject: [PATCH 15/40] spinner tests --- tests/test_cli_spinner.py | 116 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/test_cli_spinner.py diff --git a/tests/test_cli_spinner.py b/tests/test_cli_spinner.py new file mode 100644 index 00000000..b0bad8a4 --- /dev/null +++ b/tests/test_cli_spinner.py @@ -0,0 +1,116 @@ +import unittest +from unittest.mock import patch, MagicMock, call +import time +import threading +from io import StringIO +from agentstack.cli.spinner import Spinner + + +class TestSpinner(unittest.TestCase): + def setUp(self): + """Set up test cases.""" + self.mock_stdout_patcher = patch('sys.stdout', new_callable=StringIO) + self.mock_stdout = self.mock_stdout_patcher.start() + + self.mock_terminal_patcher = patch('shutil.get_terminal_size') + self.mock_terminal = self.mock_terminal_patcher.start() + self.mock_terminal.return_value = MagicMock(columns=80) + + # Patch the log module where Spinner is importing it + self.mock_log_patcher = patch('agentstack.cli.spinner.log') + self.mock_log = self.mock_log_patcher.start() + + def tearDown(self): + """Clean up after tests.""" + self.mock_stdout_patcher.stop() + self.mock_terminal_patcher.stop() + self.mock_log_patcher.stop() + + def test_spinner_initialization(self): + """Test spinner initialization.""" + spinner = Spinner(message="Test") + self.assertEqual(spinner.message, "Test") + self.assertEqual(spinner.delay, 0.1) + self.assertFalse(spinner.running) + self.assertIsNone(spinner.spinner_thread) + self.assertIsNone(spinner.start_time) + + def test_context_manager(self): + """Test spinner works as context manager.""" + with Spinner("Test") as spinner: + self.assertTrue(spinner.running) + self.assertTrue(spinner.spinner_thread.is_alive()) + time.sleep(0.2) + + self.assertFalse(spinner.running) + self.assertFalse(spinner.spinner_thread.is_alive()) + + def test_clear_and_log(self): + """Test clear_and_log functionality.""" + test_message = "Test log message" + with Spinner("Test") as spinner: + spinner.clear_and_log(test_message) + time.sleep(0.2) + + # Verify log.success was called with the message + self.mock_log.success.assert_called_once_with(test_message) + + def test_concurrent_logging(self): + """Test thread safety of logging while spinner is running.""" + messages = ["Message 0", "Message 1", "Message 2"] + + def log_messages(spinner): + for msg in messages: + spinner.clear_and_log(msg) + time.sleep(0.1) + + with Spinner("Test") as spinner: + thread = threading.Thread(target=log_messages, args=(spinner,)) + thread.start() + thread.join() + + # Verify all messages were logged + self.assertEqual(self.mock_log.success.call_count, len(messages)) + self.mock_log.success.assert_has_calls([call(msg) for msg in messages]) + + def test_thread_cleanup(self): + """Test proper thread cleanup after stopping.""" + spinner = Spinner("Test") + spinner.start() + time.sleep(0.2) + spinner.clear_and_log("Test message") + spinner.stop() + + # Give thread time to clean up + time.sleep(0.1) + self.assertFalse(spinner.running) + self.assertFalse(spinner.spinner_thread.is_alive()) + self.mock_log.success.assert_called_once_with("Test message") + + def test_rapid_message_updates(self): + """Test spinner handles rapid message updates and logging.""" + messages = [f"Message {i}" for i in range(5)] + with Spinner("Initial") as spinner: + for msg in messages: + spinner.update_message(msg) + spinner.clear_and_log(f"Logged: {msg}") + time.sleep(0.05) + + # Verify all messages were logged + self.assertEqual(self.mock_log.success.call_count, len(messages)) + self.mock_log.success.assert_has_calls([ + call(f"Logged: {msg}") for msg in messages + ]) + + @patch('time.time') + def test_elapsed_time_display(self, mock_time): + """Test elapsed time is displayed correctly.""" + mock_time.side_effect = [1000, 1001, 1002] # Mock timestamps + + with Spinner("Test") as spinner: + spinner.clear_and_log("Time check") + time.sleep(0.2) + output = self.mock_stdout.getvalue() + self.assertIn("[1.0s]", output) + self.mock_log.success.assert_called_once_with("Time check") + From 3fd8ef15e45c15b66ba7c585c25be5b03bdd2d3d Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 21 Jan 2025 23:42:45 -0800 Subject: [PATCH 16/40] deploy tests --- tests/test_deploy.py | 149 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 tests/test_deploy.py diff --git a/tests/test_deploy.py b/tests/test_deploy.py new file mode 100644 index 00000000..57e74603 --- /dev/null +++ b/tests/test_deploy.py @@ -0,0 +1,149 @@ +import os +import unittest +from unittest.mock import patch, Mock, mock_open +from pathlib import Path +import tempfile +import zipfile + +from agentstack.deploy import deploy, collect_files, create_zip_in_memory, get_project_id, load_pyproject + +class TestDeployFunctions(unittest.TestCase): + def setUp(self): + # Common setup for tests + self.bearer_token = "test_token" + self.project_id = "test_project_123" + + @patch('agentstack.deploy.get_stored_token') + @patch('agentstack.deploy.login') + @patch('agentstack.deploy.get_project_id') + @patch('agentstack.deploy.requests.post') + @patch('agentstack.deploy.webbrowser.open') + def test_deploy_success(self, mock_browser, mock_post, mock_get_project, mock_login, mock_token): + # Setup mocks + mock_token.return_value = self.bearer_token + mock_get_project.return_value = self.project_id + mock_post.return_value.status_code = 200 + + # Call deploy function + deploy() + + # Verify API call + mock_post.assert_called_once() + self.assertIn('/deploy/build', mock_post.call_args[0][0]) + self.assertIn('Bearer test_token', mock_post.call_args[1]['headers']['Authorization']) + + # Verify browser opened + mock_browser.assert_called_once_with(f"http://localhost:5173/project/{self.project_id}") + + @patch('agentstack.deploy.get_stored_token') + @patch('agentstack.deploy.login') + def test_deploy_no_auth(self, mock_login, mock_token): + # Setup mocks for failed authentication + mock_token.return_value = None + mock_login.return_value = False + + # Call deploy function + deploy() + + # Verify login was attempted + mock_login.assert_called_once() + mock_token.assert_called_once() + + def test_collect_files(self): + # Create temporary directory structure + with tempfile.TemporaryDirectory() as tmpdir: + # Create test files + Path(tmpdir, 'test.py').touch() + Path(tmpdir, 'test.toml').touch() + Path(tmpdir, 'ignore.txt').touch() + + # Create subdirectory with files + subdir = Path(tmpdir, 'subdir') + subdir.mkdir() + Path(subdir, 'sub.py').touch() + + # Create excluded directory + venv = Path(tmpdir, '.venv') + venv.mkdir() + Path(venv, 'venv.py').touch() + + # Collect files + files = collect_files(tmpdir, ('.py', '.toml')) + + # Verify results + file_names = {f.name for f in files} + self.assertIn('test.py', file_names) + self.assertIn('test.toml', file_names) + self.assertIn('sub.py', file_names) + self.assertNotIn('ignore.txt', file_names) + self.assertNotIn('venv.py', file_names) + + @patch('agentstack.deploy.requests.post') + def test_get_project_id_create_new(self, mock_post): + # Mock successful project creation + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {'id': 'new_project_123'} + + # Mock ConfigFile + with patch('agentstack.deploy.ConfigFile') as mock_config: + mock_config.return_value.hosted_project_id = None + mock_config.return_value.project_name = 'test_project' + + project_id = get_project_id() + + self.assertEqual(project_id, 'new_project_123') + mock_post.assert_called_once() + + def test_load_pyproject(self): + # Test with valid pyproject.toml + mock_toml_content = b''' + [project] + name = "test-project" + version = "1.0.0" + ''' + + mock_file = mock_open(read_data=mock_toml_content) + with patch('builtins.open', mock_file): + with patch('os.path.exists') as mock_exists: + mock_exists.return_value = True + result = load_pyproject() + + # Verify file was opened in binary mode + mock_file.assert_called_once_with("pyproject.toml", "rb") + + self.assertIsNotNone(result) + self.assertIn('project', result) + self.assertEqual(result['project']['name'], 'test-project') + + @patch('agentstack.deploy.log.error') + def test_create_zip_in_memory(self, mock_log): + # Create temporary test files + with tempfile.TemporaryDirectory() as tmpdir: + # Create a test directory structure + test_dir = Path(tmpdir) + test_file = test_dir / 'test.py' + test_file.write_text('print("test")') + + # Create mock spinner + mock_spinner = Mock() + + # Test zip creation + # We'll need to change to the temp directory to maintain correct relative paths + original_dir = Path.cwd() + try: + os.chdir(tmpdir) + # Now use relative path for the file + files = [Path('test.py')] + zip_file = create_zip_in_memory(files, mock_spinner) + + # Verify zip contents + with zipfile.ZipFile(zip_file, 'r') as zf: + self.assertIn('test.py', zf.namelist()) + # Additional verification of zip contents + self.assertEqual(len(zf.namelist()), 1) + with zf.open('test.py') as f: + content = f.read().decode('utf-8') + self.assertEqual(content, 'print("test")') + finally: + # Make sure we always return to the original directory + os.chdir(original_dir) From 044e053a9d705ead974a78333664c2bbd7a4da7c Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 21 Jan 2025 23:46:34 -0800 Subject: [PATCH 17/40] logging --- agentstack/deploy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/agentstack/deploy.py b/agentstack/deploy.py index 42100d31..dfdfa578 100644 --- a/agentstack/deploy.py +++ b/agentstack/deploy.py @@ -70,12 +70,13 @@ def deploy(): if response.status_code != 200: raise Exception(response.text) - spinner.clear_and_log("š Successfully deployed with AgentStack.sh! Opening in browser...") + spinner.stop() + log.success("\nš Successfully deployed with AgentStack.sh! Opening in browser...") webbrowser.open(f"http://localhost:5173/project/{project_id}") except Exception as e: spinner.stop() - log.error(f"š Failed to deploy with AgentStack.sh: {e}") + log.error(f"\nš Failed to deploy with AgentStack.sh: {e}") return def load_pyproject(): From d2590e0c8d33ea111d9ca322b578ebec9c6ef435 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Wed, 22 Jan 2025 01:08:21 -0800 Subject: [PATCH 18/40] use websockets --- agentstack/deploy.py | 51 +++++++++++++++++++++++++++----------------- agentstack/main.py | 3 ++- pyproject.toml | 3 ++- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/agentstack/deploy.py b/agentstack/deploy.py index dfdfa578..fa29e957 100644 --- a/agentstack/deploy.py +++ b/agentstack/deploy.py @@ -1,5 +1,6 @@ +import asyncio +import json import os -import sys import tempfile import time import tomllib @@ -13,9 +14,29 @@ from agentstack.utils import term_color from agentstack import log import requests +import websockets -def deploy(): +async def connect_websocket(project_id): + uri = f"ws://localhost:3000/ws/build/{project_id}" + async with websockets.connect(uri) as websocket: + try: + while True: + message = await websocket.recv() + data = json.loads(message) + if data['type'] == 'build': + log.info(f"šļø {data.get('data','')}") + elif data['type'] == 'push': + log.info(f"š¤ {data}") + elif data['type'] == 'connected': + log.info(f"\n\n~~ Build stream connected! ~~") + elif data['type'] == 'error': + raise Exception(f"Failed to deploy: {data.get('data')}") + except websockets.ConnectionClosed: + raise Exception("Websocket connection closed unexpectedly") + + +async def deploy(): log.info("Deploying your agentstack agent!") bearer_token = get_stored_token() if not bearer_token: @@ -27,21 +48,7 @@ def deploy(): return project_id = get_project_id() - pyproject = load_pyproject() - - # def should_skip_dir(path: Path) -> bool: - # skip_dirs = {'.venv', '__pycache__', '.git', 'build', 'dist'} - # return path.name in skip_dirs - - # files = list(Path('.').rglob('*.py')) - # with tempfile.NamedTemporaryFile(suffix='.zip') as tmp: - # with zipfile.ZipFile(tmp.name, 'w') as zf: - # for file in files: - # zf.write(file) - # if pyproject: - # zf.write("pyproject.toml") - - zip_file = None + websocket_task = asyncio.create_task(connect_websocket(project_id)) with Spinner() as spinner: time.sleep(0.1) @@ -65,14 +72,18 @@ def deploy(): headers={'Authorization': f'Bearer {bearer_token}'} ) - spinner.clear_and_log(" š” Uploaded to server") + spinner.clear_and_log(" š” Uploaded to server") if response.status_code != 200: raise Exception(response.text) - spinner.stop() + spinner.update_message("Building your agent") + + # Wait for build completion + await websocket_task + log.success("\nš Successfully deployed with AgentStack.sh! Opening in browser...") - webbrowser.open(f"http://localhost:5173/project/{project_id}") + # webbrowser.open(f"http://localhost:5173/project/{project_id}") except Exception as e: spinner.stop() diff --git a/agentstack/main.py b/agentstack/main.py index aa6d6515..cb4d520d 100644 --- a/agentstack/main.py +++ b/agentstack/main.py @@ -1,3 +1,4 @@ +import asyncio import sys import argparse import webbrowser @@ -201,7 +202,7 @@ def _main(): run_project(command=args.function, debug=args.debug, cli_args=extra_args) elif args.command in ['deploy', 'd']: conf.assert_project() - deploy() + asyncio.run(deploy()) elif args.command in ['generate', 'g']: conf.assert_project() if args.generate_command in ['agent', 'a']: diff --git a/pyproject.toml b/pyproject.toml index 2fa0a999..362a0708 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,8 @@ dependencies = [ "appdirs>=1.4.4", "python-dotenv>=1.0.1", "uv>=0.5.6", - "tomli>=2.2.1" + "tomli>=2.2.1", + "websockets>=14.2" ] [project.optional-dependencies] From 325d76e78b45dc861d700b6636b2ca9921806d42 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Thu, 23 Jan 2025 13:19:55 -0800 Subject: [PATCH 19/40] use proper dockerfile --- agentstack/cli/cli.py | 2 +- agentstack/cli/spinner.py | 9 +++++++-- agentstack/deploy.py | 10 +++++----- agentstack/serve/Dockerfile | 18 +++++++++--------- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/agentstack/cli/cli.py b/agentstack/cli/cli.py index c3187280..38421bbb 100644 --- a/agentstack/cli/cli.py +++ b/agentstack/cli/cli.py @@ -19,7 +19,7 @@ ) from agentstack import conf, log from agentstack.conf import ConfigFile -from agentstack.utils import get_package_path +from agentstack.utils import get_package_path, verify_agentstack_project from agentstack.generation.files import ProjectFile from agentstack import frameworks from agentstack import generation diff --git a/agentstack/cli/spinner.py b/agentstack/cli/spinner.py index 7fdcad71..fb06f371 100644 --- a/agentstack/cli/spinner.py +++ b/agentstack/cli/spinner.py @@ -3,6 +3,8 @@ import sys import threading import time +from typing import Optional, Literal + from agentstack import log @@ -73,9 +75,12 @@ def update_message(self, message): self._clear_line() self.message = message - def clear_and_log(self, message): + def clear_and_log(self, message, color: Literal['success','info'] = 'success'): """Temporarily clear spinner, print message, and resume spinner.""" with self._lock: self._clear_line() - log.success(message) + if color == 'success': + log.success(message) + else: + log.info(message) sys.stdout.flush() \ No newline at end of file diff --git a/agentstack/deploy.py b/agentstack/deploy.py index fa29e957..8c8f578e 100644 --- a/agentstack/deploy.py +++ b/agentstack/deploy.py @@ -17,7 +17,7 @@ import websockets -async def connect_websocket(project_id): +async def connect_websocket(project_id, spinner): uri = f"ws://localhost:3000/ws/build/{project_id}" async with websockets.connect(uri) as websocket: try: @@ -25,11 +25,11 @@ async def connect_websocket(project_id): message = await websocket.recv() data = json.loads(message) if data['type'] == 'build': - log.info(f"šļø {data.get('data','')}") + spinner.clear_and_log(f"šļø {data.get('data','')}", 'info') elif data['type'] == 'push': - log.info(f"š¤ {data}") + spinner.clear_and_log(f"š¤ {data.get('data','')}", 'info') elif data['type'] == 'connected': - log.info(f"\n\n~~ Build stream connected! ~~") + spinner.clear_and_log(f"\n\n~~ Build stream connected! ~~") elif data['type'] == 'error': raise Exception(f"Failed to deploy: {data.get('data')}") except websockets.ConnectionClosed: @@ -48,9 +48,9 @@ async def deploy(): return project_id = get_project_id() - websocket_task = asyncio.create_task(connect_websocket(project_id)) with Spinner() as spinner: + websocket_task = asyncio.create_task(connect_websocket(project_id, spinner)) time.sleep(0.1) try: spinner.update_message("Collecting files") diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index 1795b78c..d89bbde7 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -1,29 +1,29 @@ -# 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 && \ +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 --no-cache-dir uv RUN pip install psutil flask RUN #pip install agentstack -RUN pip install git+https://github.com/bboynton97/AgentStack.git@deploy +RUN pip install git+https://github.com/bboynton97/AgentStack.git@deploy-command 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 +RUN uv venv +RUN source .venv/bin/activate +RUN uv pip install --requirements pyproject.toml # Copy the rest of the application COPY . . @@ -32,4 +32,4 @@ COPY . . EXPOSE 6969 # Command to run the application -CMD ["python", "src/serve.py"] \ No newline at end of file +CMD ["agentstack", "run"] \ No newline at end of file From 4b31001a9bbaf8b09ccae910eab29b0a13b8ff98 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 4 Feb 2025 16:45:54 -0800 Subject: [PATCH 20/40] build, push, track, webhooks --- agentstack/cli/__init__.py | 2 +- agentstack/cli/spinner.py | 15 ++++++++++++--- agentstack/deploy.py | 4 +++- agentstack/main.py | 3 ++- agentstack/serve/Dockerfile | 17 +++++++---------- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/agentstack/cli/__init__.py b/agentstack/cli/__init__.py index a7ce062a..243aa474 100644 --- a/agentstack/cli/__init__.py +++ b/agentstack/cli/__init__.py @@ -1,4 +1,4 @@ -from .cli import init_project_builder, configure_default_model, welcome_message, get_validated_input +from .cli import configure_default_model, welcome_message, get_validated_input from .init import init_project from .wizard import run_wizard from .run import run_project diff --git a/agentstack/cli/spinner.py b/agentstack/cli/spinner.py index fb06f371..dc70ba58 100644 --- a/agentstack/cli/spinner.py +++ b/agentstack/cli/spinner.py @@ -18,6 +18,7 @@ def __init__(self, message="Working", delay=0.1): self.start_time = None self._lock = threading.Lock() self._last_printed_len = 0 + self._last_message = "" def _clear_line(self): """Clear the current line in terminal.""" @@ -75,12 +76,20 @@ def update_message(self, message): self._clear_line() self.message = message - def clear_and_log(self, message, color: Literal['success','info'] = 'success'): - """Temporarily clear spinner, print message, and resume spinner.""" + def clear_and_log(self, message, color: Literal['success', 'info'] = 'success'): + """Temporarily clear spinner, print message, and resume spinner. + Skips printing if message is the same as the last message printed.""" with self._lock: + # Skip if message is same as last one + if hasattr(self, '_last_message') and self._last_message == message: + return + self._clear_line() if color == 'success': log.success(message) else: log.info(message) - sys.stdout.flush() \ No newline at end of file + sys.stdout.flush() + + # Store current message + self._last_message = message \ No newline at end of file diff --git a/agentstack/deploy.py b/agentstack/deploy.py index 8c8f578e..fe0e17d3 100644 --- a/agentstack/deploy.py +++ b/agentstack/deploy.py @@ -30,6 +30,8 @@ async def connect_websocket(project_id, spinner): spinner.clear_and_log(f"š¤ {data.get('data','')}", 'info') elif data['type'] == 'connected': spinner.clear_and_log(f"\n\n~~ Build stream connected! ~~") + elif data['type'] == 'deploy': + spinner.clear_and_log(f"š {data.get('data','')}", 'info') elif data['type'] == 'error': raise Exception(f"Failed to deploy: {data.get('data')}") except websockets.ConnectionClosed: @@ -77,7 +79,7 @@ async def deploy(): if response.status_code != 200: raise Exception(response.text) - spinner.update_message("Building your agent") + spinner.update_message("Building and deploying your agent") # Wait for build completion await websocket_task diff --git a/agentstack/main.py b/agentstack/main.py index 70c61405..04be6deb 100644 --- a/agentstack/main.py +++ b/agentstack/main.py @@ -12,8 +12,9 @@ configure_default_model, run_project, export_template, - serve_project + # serve_project ) +from agentstack.cli.cli import serve_project from agentstack.telemetry import track_cli_command, update_telemetry from agentstack.utils import get_version, term_color from agentstack import generation diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index d89bbde7..57724df0 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -1,12 +1,10 @@ -FROM python:3.11-slim +FROM python:3.12-slim-bookworm +RUN rm /bin/sh && ln -s /bin/bash /bin/sh 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/* +RUN apt update && apt install -y git gcc build-essential tree +RUN apt clean # Copy requirements first to leverage Docker cache COPY pyproject.toml . @@ -14,13 +12,12 @@ RUN pip install --no-cache-dir uv RUN pip install psutil flask RUN #pip install agentstack -RUN pip install git+https://github.com/bboynton97/AgentStack.git@deploy-command +RUN pip install git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command RUN mkdir src -RUN cp /usr/local/lib/python3.11/site-packages/agentstack/deploy/serve.py ./src +RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src RUN pip uninstall -y agentstack -RUN apt-get update && apt-get install -y gcc RUN uv venv RUN source .venv/bin/activate RUN uv pip install --requirements pyproject.toml @@ -32,4 +29,4 @@ COPY . . EXPOSE 6969 # Command to run the application -CMD ["agentstack", "run"] \ No newline at end of file +CMD ["/app/.venv/bin/agentstack", "run"] \ No newline at end of file From 6a3028027962f6cd68abe838a3363051af8a2408 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Wed, 5 Feb 2025 23:44:24 -0800 Subject: [PATCH 21/40] serve work --- MANIFEST.in | 1 + agentstack/cli/cli.py | 2 +- agentstack/serve/Dockerfile | 37 +++++++++++++++++-------------------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 1f7cc4f8..860c8d76 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ recursive-include agentstack/templates * recursive-include agentstack/_tools * +recursive-include agentstack/serve/serve.py include agentstack.json .env .env.example \ No newline at end of file diff --git a/agentstack/cli/cli.py b/agentstack/cli/cli.py index 675c2fdc..cd0373fe 100644 --- a/agentstack/cli/cli.py +++ b/agentstack/cli/cli.py @@ -391,6 +391,6 @@ def serve_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: + with importlib.resources.path('agentstack.serve', '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/serve/Dockerfile b/agentstack/serve/Dockerfile index 57724df0..3dbceb7d 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -6,27 +6,24 @@ WORKDIR /app RUN apt update && apt install -y git gcc build-essential tree RUN apt clean -# Copy requirements first to leverage Docker cache -COPY pyproject.toml . -RUN pip install --no-cache-dir uv -RUN pip install psutil flask - -RUN #pip install agentstack -RUN pip install git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command -RUN mkdir src -RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src - -RUN pip uninstall -y agentstack - -RUN uv venv -RUN source .venv/bin/activate -RUN uv pip install --requirements pyproject.toml - -# Copy the rest of the application COPY . . -# Expose the port the app runs on +# Install uv and create venv in one layer +RUN pip install --no-cache-dir uv \ + && uv venv \ + && . .venv/bin/activate \ + && uv pip install psutil flask \ + && uv pip install git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command \ + && mkdir -p src \ + && cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src \ + && uv pip uninstall -y agentstack \ + && uv pip install . + +# Add venv to path +ENV PATH="/app/.venv/bin:$PATH" + +# Expose the port EXPOSE 6969 -# Command to run the application -CMD ["/app/.venv/bin/agentstack", "run"] \ No newline at end of file +# Use absolute path to be safe +CMD ["/app/.venv/bin/python", "src/serve.py"] \ No newline at end of file From 0ad98ea9e3058231f887d82b28ba0bb1614df75b Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Thu, 6 Feb 2025 15:21:29 -0800 Subject: [PATCH 22/40] dockerfile works and new run func --- agentstack/cli/cli.py | 2 +- agentstack/cli/run.py | 7 +++++-- agentstack/serve/Dockerfile | 29 ++++++++++++++--------------- agentstack/serve/serve.py | 7 +++++-- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/agentstack/cli/cli.py b/agentstack/cli/cli.py index cd0373fe..3df264a4 100644 --- a/agentstack/cli/cli.py +++ b/agentstack/cli/cli.py @@ -392,5 +392,5 @@ def serve_project(): 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.serve', 'Dockerfile') as path: - os.system(f"docker build -t agent-service -f {path} .") + os.system(f"docker build -t agent-service -f {path} . --progress=plain") os.system("docker run --name agentstack-local -p 6969:6969 agent-service") \ No newline at end of file diff --git a/agentstack/cli/run.py b/agentstack/cli/run.py index fa82d4d2..ef61c04f 100644 --- a/agentstack/cli/run.py +++ b/agentstack/cli/run.py @@ -1,4 +1,4 @@ -from typing import Optional, List +from typing import Optional, List, Dict import sys import traceback from pathlib import Path @@ -93,7 +93,7 @@ def _import_project_module(path: Path): return project_module -def run_project(command: str = 'run', cli_args: Optional[List[str]] = None): +def run_project(command: str = 'run', cli_args: Optional[List[str]] = None, api_inputs: Optional[Dict[str, str]] = None): """Validate that the project is ready to run and then run it.""" verify_agentstack_project() @@ -114,6 +114,9 @@ def run_project(command: str = 'run', cli_args: Optional[List[str]] = None): log.debug(f"Using CLI input override: {key}={value}") inputs.add_input_for_run(key, value) + if api_inputs: + inputs.add_input_for_run(**api_inputs) + load_dotenv(Path.home() / '.env') # load the user's .env file load_dotenv(conf.PATH / '.env', override=True) # load the project's .env file diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index 3dbceb7d..68fd8e3a 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -6,24 +6,23 @@ WORKDIR /app RUN apt update && apt install -y git gcc build-essential tree RUN apt clean -COPY . . +# Install uv +RUN pip install --no-cache-dir uv -# Install uv and create venv in one layer -RUN pip install --no-cache-dir uv \ - && uv venv \ - && . .venv/bin/activate \ - && uv pip install psutil flask \ - && uv pip install git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command \ - && mkdir -p src \ - && cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src \ - && uv pip uninstall -y agentstack \ - && uv pip install . +# Copy everything since we're installing local package +COPY . . -# Add venv to path -ENV PATH="/app/.venv/bin:$PATH" +# Debug: Try installing packages one at a time +RUN uv pip install --system psutil +RUN uv pip install --system flask +RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command +RUN mkdir -p src +RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src +RUN #uv pip uninstall -y agentstack +RUN uv pip install --system . # Expose the port EXPOSE 6969 -# Use absolute path to be safe -CMD ["/app/.venv/bin/python", "src/serve.py"] \ No newline at end of file +# Use python directly since we're installing to system +CMD ["python", "src/serve.py"] \ No newline at end of file diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index 587bc01e..d453e1b0 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -6,7 +6,7 @@ import requests from typing import Dict, Any import os -from main import run +from run import run_project app = Flask(__name__) @@ -34,13 +34,16 @@ def process_agent(): if not request_data or 'webhook_url' not in request_data: return jsonify({'error': 'Missing webhook_url in request'}), 400 + if not request_data or 'inputs' not in request_data: + return jsonify({'error': 'Missing input data 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) + result = run_project(api_inputs=request_data) # Call the webhook with the results call_webhook(webhook_url, { From 2b94a9f7f9a76d2b0d37ae7055e5bdca6a7a37c0 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Thu, 6 Feb 2025 15:42:42 -0800 Subject: [PATCH 23/40] import fix --- agentstack/serve/Dockerfile | 8 +++++++- agentstack/serve/serve.py | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index 68fd8e3a..d5054547 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -11,13 +11,19 @@ RUN pip install --no-cache-dir uv # Copy everything since we're installing local package COPY . . +# install local version of agentstack for local dev +#COPY ../../ agentstack +RUN #ls -a agentstack + # Debug: Try installing packages one at a time RUN uv pip install --system psutil RUN uv pip install --system flask -RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command +RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command #install on cloud +RUN #cd agentstack && uv pip install --system . RUN mkdir -p src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src +RUN #cp agentstack/serve/serve.py ./src RUN #uv pip uninstall -y agentstack RUN uv pip install --system . diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index d453e1b0..8d9693de 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -6,7 +6,7 @@ import requests from typing import Dict, Any import os -from run import run_project +from agentstack.run import run_project app = Flask(__name__) @@ -43,7 +43,7 @@ def process_agent(): # result = WebresearcherCrew().crew().kickoff(inputs=request_data) # inputs = json.stringify(request_data) # os.system(f"python src/main.py {inputs}") - result = run_project(api_inputs=request_data) + result = run_project(api_inputs=request_data.get('inputs')) # Call the webhook with the results call_webhook(webhook_url, { From 023b3ce53406e883bde45d839c202565ac11337e Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Thu, 6 Feb 2025 16:07:34 -0800 Subject: [PATCH 24/40] run proj from serve.py --- agentstack/cli/run.py | 9 ++---- agentstack/log.py | 1 - agentstack/serve/Dockerfile | 1 + agentstack/serve/serve.py | 61 +++++++++++++++++++++++++++++++++++-- 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/agentstack/cli/run.py b/agentstack/cli/run.py index ef61c04f..0faae7c5 100644 --- a/agentstack/cli/run.py +++ b/agentstack/cli/run.py @@ -15,7 +15,7 @@ MAIN_MODULE_NAME = "main" -def _format_friendly_error_message(exception: Exception): +def format_friendly_error_message(exception: Exception): """ Projects will throw various errors, especially on first runs, so we catch them here and print a more helpful message. @@ -93,7 +93,7 @@ def _import_project_module(path: Path): return project_module -def run_project(command: str = 'run', cli_args: Optional[List[str]] = None, api_inputs: Optional[Dict[str, str]] = None): +def run_project(command: str = 'run', cli_args: Optional[List[str]] = None): """Validate that the project is ready to run and then run it.""" verify_agentstack_project() @@ -114,9 +114,6 @@ def run_project(command: str = 'run', cli_args: Optional[List[str]] = None, api_ log.debug(f"Using CLI input override: {key}={value}") inputs.add_input_for_run(key, value) - if api_inputs: - inputs.add_input_for_run(**api_inputs) - load_dotenv(Path.home() / '.env') # load the user's .env file load_dotenv(conf.PATH / '.env', override=True) # load the project's .env file @@ -128,4 +125,4 @@ def run_project(command: str = 'run', cli_args: Optional[List[str]] = None, api_ except ImportError as e: raise ValidationError(f"Failed to import AgentStack project at: {conf.PATH.absolute()}\n{e}") except Exception as e: - raise Exception(_format_friendly_error_message(e)) + raise Exception(format_friendly_error_message(e)) diff --git a/agentstack/log.py b/agentstack/log.py index af3ca697..a272b8c6 100644 --- a/agentstack/log.py +++ b/agentstack/log.py @@ -24,7 +24,6 @@ """ from typing import IO, Optional, Callable -import os, sys import io import logging from agentstack import conf diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index d5054547..e2a4c109 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -19,6 +19,7 @@ RUN #ls -a agentstack # Debug: Try installing packages one at a time RUN uv pip install --system psutil RUN uv pip install --system flask +# Cache-buster: 2 RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command #install on cloud RUN #cd agentstack && uv pip install --system . RUN mkdir -p src diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index 8d9693de..b1e2c696 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -1,12 +1,23 @@ # app.py +import importlib +import sys +from pathlib import Path from dotenv import load_dotenv +from agentstack import conf, frameworks, inputs +from agentstack.exceptions import ValidationError +from agentstack.utils import verify_agentstack_project +# TODO: move this to not cli, but cant be utils due to circular import +from agentstack.cli.run import format_friendly_error_message + load_dotenv(dotenv_path="/app/.env") from flask import Flask, request, jsonify import requests -from typing import Dict, Any +from typing import Dict, Any, Optional import os -from agentstack.run import run_project + +MAIN_FILENAME: Path = Path("src/main.py") +MAIN_MODULE_NAME = "main" app = Flask(__name__) @@ -84,3 +95,49 @@ def process_agent(): 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) + + +def run_project(command: str = 'run', api_args: Optional[Dict[str, str]] = None, + api_inputs: Optional[Dict[str, str]] = None): + """Validate that the project is ready to run and then run it.""" + verify_agentstack_project() + + if conf.get_framework() not in frameworks.SUPPORTED_FRAMEWORKS: + raise ValidationError(f"Framework {conf.get_framework()} is not supported by agentstack.") + + try: + frameworks.validate_project() + except ValidationError as e: + raise e + + inputs.add_input_for_run(**api_inputs) + + load_dotenv(Path.home() / '.env') # load the user's .env file + load_dotenv(conf.PATH / '.env', override=True) # load the project's .env file + + # import src/main.py from the project path and run `command` from the project's main.py + try: + log.notify("Running your agent...") + project_main = _import_project_module(conf.PATH) + getattr(project_main, command)() + except ImportError as e: + raise ValidationError(f"Failed to import AgentStack project at: {conf.PATH.absolute()}\n{e}") + except Exception as e: + raise Exception(format_friendly_error_message(e)) + +def _import_project_module(path: Path): + """ + Import `main` from the project path. + + We do it this way instead of spawning a subprocess so that we can share + state with the user's project. + """ + spec = importlib.util.spec_from_file_location(MAIN_MODULE_NAME, str(path / MAIN_FILENAME)) + + assert spec is not None # appease type checker + assert spec.loader is not None # appease type checker + + project_module = importlib.util.module_from_spec(spec) + sys.path.insert(0, str((path / MAIN_FILENAME).parent)) + spec.loader.exec_module(project_module) + return project_module \ No newline at end of file From 72161d34959385398bf9ce2978293f3faba9fa60 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Thu, 6 Feb 2025 16:10:31 -0800 Subject: [PATCH 25/40] if main at bottom --- agentstack/serve/Dockerfile | 3 +-- agentstack/serve/serve.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index e2a4c109..4e0fcf4e 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -19,8 +19,7 @@ RUN #ls -a agentstack # Debug: Try installing packages one at a time RUN uv pip install --system psutil RUN uv pip install --system flask -# Cache-buster: 2 -RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command #install on cloud +RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command #install on cloud #cache 1 RUN #cd agentstack && uv pip install --system . RUN mkdir -p src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index b1e2c696..b5540d53 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -87,16 +87,6 @@ def process_agent(): }), 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) - - def run_project(command: str = 'run', api_args: Optional[Dict[str, str]] = None, api_inputs: Optional[Dict[str, str]] = None): """Validate that the project is ready to run and then run it.""" @@ -140,4 +130,14 @@ def _import_project_module(path: Path): project_module = importlib.util.module_from_spec(spec) sys.path.insert(0, str((path / MAIN_FILENAME).parent)) spec.loader.exec_module(project_module) - return project_module \ No newline at end of file + return project_module + + +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) \ No newline at end of file From 35d94a4b629d24daa073e34cbc9c8b537c0e3921 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Thu, 6 Feb 2025 16:13:08 -0800 Subject: [PATCH 26/40] input handling --- agentstack/serve/Dockerfile | 2 +- agentstack/serve/serve.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index 4e0fcf4e..e03015f5 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -19,7 +19,7 @@ RUN #ls -a agentstack # Debug: Try installing packages one at a time RUN uv pip install --system psutil RUN uv pip install --system flask -RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command #install on cloud #cache 1 +RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command #install on cloud #cache 2 RUN #cd agentstack && uv pip install --system . RUN mkdir -p src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index b5540d53..e82389c5 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -100,7 +100,8 @@ def run_project(command: str = 'run', api_args: Optional[Dict[str, str]] = None, except ValidationError as e: raise e - inputs.add_input_for_run(**api_inputs) + for key, value in api_inputs.items(): + inputs.add_input_for_run(key, value) load_dotenv(Path.home() / '.env') # load the user's .env file load_dotenv(conf.PATH / '.env', override=True) # load the project's .env file From 0a7eba710cc57fc45cd313dafde5de7b0b778a5d Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Thu, 6 Feb 2025 16:14:47 -0800 Subject: [PATCH 27/40] log import --- agentstack/serve/Dockerfile | 2 +- agentstack/serve/serve.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index e03015f5..6549da0c 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -19,7 +19,7 @@ RUN #ls -a agentstack # Debug: Try installing packages one at a time RUN uv pip install --system psutil RUN uv pip install --system flask -RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command #install on cloud #cache 2 +RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command #install on cloud #cache 3 RUN #cd agentstack && uv pip install --system . RUN mkdir -p src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index e82389c5..70106c29 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -8,6 +8,7 @@ from agentstack.utils import verify_agentstack_project # TODO: move this to not cli, but cant be utils due to circular import from agentstack.cli.run import format_friendly_error_message +from build.lib.agentstack.logger import log load_dotenv(dotenv_path="/app/.env") From cf49ff8e3bb25ce0fb17d4364f5c2eb1f7ac0bcf Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Thu, 6 Feb 2025 16:17:35 -0800 Subject: [PATCH 28/40] log import fix --- agentstack/serve/Dockerfile | 2 +- agentstack/serve/serve.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index 6549da0c..e48ce8a5 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -19,7 +19,7 @@ RUN #ls -a agentstack # Debug: Try installing packages one at a time RUN uv pip install --system psutil RUN uv pip install --system flask -RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command #install on cloud #cache 3 +RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command #install on cloud #cache 4 RUN #cd agentstack && uv pip install --system . RUN mkdir -p src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index 70106c29..5d7c30f6 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -3,12 +3,11 @@ import sys from pathlib import Path from dotenv import load_dotenv -from agentstack import conf, frameworks, inputs +from agentstack import conf, frameworks, inputs, log from agentstack.exceptions import ValidationError from agentstack.utils import verify_agentstack_project # TODO: move this to not cli, but cant be utils due to circular import from agentstack.cli.run import format_friendly_error_message -from build.lib.agentstack.logger import log load_dotenv(dotenv_path="/app/.env") From bf7fd6e1bcb3b0b328531bad5d3f9ce9f18f46f1 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Thu, 6 Feb 2025 16:26:29 -0800 Subject: [PATCH 29/40] todos --- agentstack/serve/Dockerfile | 2 +- agentstack/serve/serve.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index e48ce8a5..d4053ed9 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -19,7 +19,7 @@ RUN #ls -a agentstack # Debug: Try installing packages one at a time RUN uv pip install --system psutil RUN uv pip install --system flask -RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command #install on cloud #cache 4 +RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command #install on cloud #cache 5 RUN #cd agentstack && uv pip install --system . RUN mkdir -p src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index 5d7c30f6..b0584513 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -42,6 +42,8 @@ def process_agent(): try: # Extract data and webhook URL from request request_data = request.get_json() + + # TODO: validate webhook url if not request_data or 'webhook_url' not in request_data: return jsonify({'error': 'Missing webhook_url in request'}), 400 @@ -54,6 +56,9 @@ def process_agent(): # result = WebresearcherCrew().crew().kickoff(inputs=request_data) # inputs = json.stringify(request_data) # os.system(f"python src/main.py {inputs}") + + # TODO: run in subprocess so we can return started and then webhook called with callback later + # TODO: only allow one process to run at a time per pod result = run_project(api_inputs=request_data.get('inputs')) # Call the webhook with the results From 4b5a178f5db8c9fcb836ecebed119ad0d68f874f Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Mon, 10 Feb 2025 15:46:33 -0800 Subject: [PATCH 30/40] update serve --- agentstack/serve/Dockerfile | 32 +++++++++++++------- agentstack/serve/serve.py | 59 ++++++++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index d4053ed9..9410358c 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -11,24 +11,34 @@ RUN pip install --no-cache-dir uv # Copy everything since we're installing local package COPY . . -# install local version of agentstack for local dev -#COPY ../../ agentstack -RUN #ls -a agentstack - -# Debug: Try installing packages one at a time RUN uv pip install --system psutil RUN uv pip install --system flask -RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command #install on cloud #cache 5 -RUN #cd agentstack && uv pip install --system . +RUN uv pip install --system gunicorn +RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command RUN mkdir -p src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src -RUN #cp agentstack/serve/serve.py ./src -RUN #uv pip uninstall -y agentstack RUN uv pip install --system . +# Create Gunicorn config +RUN echo 'import multiprocessing\n\ +bind = "0.0.0.0:6969"\n\ +workers = 1\n\ +threads = 1\n\ +worker_class = "sync"\n\ +max_requests = 1\n\ +max_requests_jitter = 0\n\ +timeout = 300\n\ +keepalive = 2\n\ +worker_connections = 1\n\ +errorlog = "-"\n\ +accesslog = "-"\n\ +capture_output = True\n\ +def post_worker_init(worker):\n\ + worker.nr = 1' > gunicorn.conf.py + # Expose the port EXPOSE 6969 -# Use python directly since we're installing to system -CMD ["python", "src/serve.py"] \ No newline at end of file +# Use Gunicorn with config file +CMD ["gunicorn", "--config", "gunicorn.conf.py", "src.serve:app"] \ No newline at end of file diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index b0584513..42cc136b 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -2,23 +2,23 @@ import importlib import sys from pathlib import Path +from urllib.parse import urlparse + from dotenv import load_dotenv from agentstack import conf, frameworks, inputs, log from agentstack.exceptions import ValidationError from agentstack.utils import verify_agentstack_project # TODO: move this to not cli, but cant be utils due to circular import from agentstack.cli.run import format_friendly_error_message - -load_dotenv(dotenv_path="/app/.env") - from flask import Flask, request, jsonify import requests -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, Tuple import os MAIN_FILENAME: Path = Path("src/main.py") MAIN_MODULE_NAME = "main" +load_dotenv(dotenv_path="/app/.env") app = Flask(__name__) @@ -43,9 +43,10 @@ def process_agent(): # Extract data and webhook URL from request request_data = request.get_json() - # TODO: validate webhook url if not request_data or 'webhook_url' not in request_data: - return jsonify({'error': 'Missing webhook_url in request'}), 400 + result, message = validate_url(request_data.get("webhook_url")) + if not result: + return jsonify({'error': f'Invalid webhook_url in request: {message}'}), 400 if not request_data or 'inputs' not in request_data: return jsonify({'error': 'Missing input data in request'}), 400 @@ -139,6 +140,47 @@ def _import_project_module(path: Path): return project_module +def validate_url(url: str) -> Tuple[bool, str]: + """ + Validates a URL and returns a tuple of (is_valid, error_message). + + Args: + url (str): The URL to validate + + Returns: + Tuple[bool, str]: A tuple containing: + - Boolean indicating if the URL is valid + - Error message (empty string if valid) + """ + # Check if URL is empty + if not url: + return False, "URL cannot be empty" + + try: + # Parse the URL + result = urlparse(url) + + # Check for required components + if not result.scheme: + return False, "Missing protocol (e.g., http:// or https://)" + + if not result.netloc: + return False, "Missing domain name" + + # Validate scheme + if result.scheme not in ['http', 'https']: + return False, f"Invalid protocol: {result.scheme}" + + # Basic domain validation + if '.' not in result.netloc: + return False, "Invalid domain format" + + return True, "" + + except Exception as e: + return False, f"Invalid URL format: {str(e)}" + + if __name__ == '__main__': port = int(os.environ.get('PORT', 6969)) @@ -146,4 +188,7 @@ def _import_project_module(path: Path): 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) \ No newline at end of file + app.run(host='0.0.0.0', port=port) +else: + # This branch is used by Gunicorn + print("Starting production server with Gunicorn") \ No newline at end of file From 8c7ea35cdcc0c800f69c9161929d1f51c344edbd Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Mon, 10 Feb 2025 16:50:41 -0800 Subject: [PATCH 31/40] use gunincorn --- agentstack/cli/cli.py | 5 ++--- agentstack/serve/Dockerfile | 23 ++++------------------- agentstack/serve/gunicorn.conf.py | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 22 deletions(-) create mode 100644 agentstack/serve/gunicorn.conf.py diff --git a/agentstack/cli/cli.py b/agentstack/cli/cli.py index 2f72b2be..84ed91e6 100644 --- a/agentstack/cli/cli.py +++ b/agentstack/cli/cli.py @@ -13,15 +13,14 @@ from agentstack import conf, log from agentstack.cli.agentstack_data import CookiecutterData, ProjectStructure, ProjectMetadata, FrameworkData from agentstack.conf import ConfigFile -from agentstack.generation.files import ProjectFile, InsertionPoint +from agentstack.generation import InsertionPoint, 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 get_package_path, open_json_file, term_color, is_snake_case, get_framework, \ validator_not_empty, verify_agentstack_project -from agentstack.proj_templates import TemplateConfig +from agentstack.templates import TemplateConfig from agentstack.exceptions import ValidationError from agentstack.utils import validator_not_empty, is_snake_case diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index 9410358c..a27000a6 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -4,7 +4,7 @@ RUN rm /bin/sh && ln -s /bin/bash /bin/sh WORKDIR /app RUN apt update && apt install -y git gcc build-essential tree -RUN apt clean +RUN apt clean && rm -rf /var/lib/apt/lists/* # Install uv RUN pip install --no-cache-dir uv @@ -15,27 +15,12 @@ COPY . . RUN uv pip install --system psutil RUN uv pip install --system flask RUN uv pip install --system gunicorn -RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command +RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command # cache buster 1 RUN mkdir -p src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src +RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/gunicorn.config.py ./src RUN uv pip install --system . - -# Create Gunicorn config -RUN echo 'import multiprocessing\n\ -bind = "0.0.0.0:6969"\n\ -workers = 1\n\ -threads = 1\n\ -worker_class = "sync"\n\ -max_requests = 1\n\ -max_requests_jitter = 0\n\ -timeout = 300\n\ -keepalive = 2\n\ -worker_connections = 1\n\ -errorlog = "-"\n\ -accesslog = "-"\n\ -capture_output = True\n\ -def post_worker_init(worker):\n\ - worker.nr = 1' > gunicorn.conf.py +RUN rm -rf /root/.cache/uv/* /root/.cache/pip/* /tmp/* # Expose the port EXPOSE 6969 diff --git a/agentstack/serve/gunicorn.conf.py b/agentstack/serve/gunicorn.conf.py new file mode 100644 index 00000000..a6c32322 --- /dev/null +++ b/agentstack/serve/gunicorn.conf.py @@ -0,0 +1,18 @@ +import multiprocessing +import os + +bind = f"0.0.0.0:{os.getenv('PORT') or '6969'}" +workers = 1 +threads = 1 +worker_class = "sync" +max_requests = 1 +max_requests_jitter = 0 +timeout = 300 +keepalive = 2 +worker_connections = 1 +errorlog = "-" +accesslog = "-" +capture_output = True + +def post_worker_init(worker): + worker.nr = 1 \ No newline at end of file From f578b00bfcc4f4a8cea19a12a63f3f820f2e87f5 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 11 Feb 2025 20:45:05 -0800 Subject: [PATCH 32/40] fix name --- agentstack/serve/Dockerfile | 2 +- agentstack/serve/{gunicorn.conf.py => gunicorn.config.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename agentstack/serve/{gunicorn.conf.py => gunicorn.config.py} (100%) diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index a27000a6..ae61cf9f 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -15,7 +15,7 @@ COPY . . RUN uv pip install --system psutil RUN uv pip install --system flask RUN uv pip install --system gunicorn -RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command # cache buster 1 +RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command # cache buster 2 RUN mkdir -p src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/gunicorn.config.py ./src diff --git a/agentstack/serve/gunicorn.conf.py b/agentstack/serve/gunicorn.config.py similarity index 100% rename from agentstack/serve/gunicorn.conf.py rename to agentstack/serve/gunicorn.config.py From 3cdb963d9a7d7e9e6fa7e6e63a5b22774409d7af Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 11 Feb 2025 21:02:58 -0800 Subject: [PATCH 33/40] return before processing --- agentstack/serve/serve.py | 97 +++++++++++++-------------------------- 1 file changed, 33 insertions(+), 64 deletions(-) diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index 42cc136b..4a77c39c 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -1,4 +1,3 @@ -# app.py import importlib import sys from pathlib import Path @@ -21,6 +20,7 @@ load_dotenv(dotenv_path="/app/.env") app = Flask(__name__) +current_webhook_url = None def call_webhook(webhook_url: str, data: Dict[str, Any]) -> None: """Send results to the specified webhook URL.""" @@ -31,16 +31,16 @@ def call_webhook(webhook_url: str, data: Dict[str, Any]) -> None: 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(): + global current_webhook_url + + request_data = None try: - # Extract data and webhook URL from request request_data = request.get_json() if not request_data or 'webhook_url' not in request_data: @@ -51,47 +51,41 @@ def process_agent(): if not request_data or 'inputs' not in request_data: return jsonify({'error': 'Missing input data 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}") - - # TODO: run in subprocess so we can return started and then webhook called with callback later - # TODO: only allow one process to run at a time per pod - result = run_project(api_inputs=request_data.get('inputs')) - - # Call the webhook with the results - call_webhook(webhook_url, { - 'status': 'success', - 'result': result - }) + current_webhook_url = request_data.pop('webhook_url') return jsonify({ - 'status': 'success', - 'message': 'Agent process completed and webhook called' - }) + 'status': 'accepted', + 'message': 'Agent process started' + }), 202 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 + finally: + if current_webhook_url: + try: + result = run_project(api_inputs=request_data.get('inputs')) + call_webhook(current_webhook_url, { + 'status': 'success', + 'result': result + }) + except Exception as e: + error_message = str(e) + app.logger.error(f"Error in process: {error_message}") + try: + call_webhook(current_webhook_url, { + 'status': 'error', + 'error': error_message + }) + except: + app.logger.error("Failed to send error to webhook") + finally: + current_webhook_url = None def run_project(command: str = 'run', api_args: Optional[Dict[str, str]] = None, api_inputs: Optional[Dict[str, str]] = None): @@ -112,7 +106,6 @@ def run_project(command: str = 'run', api_args: Optional[Dict[str, str]] = None, load_dotenv(Path.home() / '.env') # load the user's .env file load_dotenv(conf.PATH / '.env', override=True) # load the project's .env file - # import src/main.py from the project path and run `command` from the project's main.py try: log.notify("Running your agent...") project_main = _import_project_module(conf.PATH) @@ -123,55 +116,34 @@ def run_project(command: str = 'run', api_args: Optional[Dict[str, str]] = None, raise Exception(format_friendly_error_message(e)) def _import_project_module(path: Path): - """ - Import `main` from the project path. - - We do it this way instead of spawning a subprocess so that we can share - state with the user's project. - """ + """Import `main` from the project path.""" spec = importlib.util.spec_from_file_location(MAIN_MODULE_NAME, str(path / MAIN_FILENAME)) - assert spec is not None # appease type checker - assert spec.loader is not None # appease type checker + assert spec is not None + assert spec.loader is not None project_module = importlib.util.module_from_spec(spec) sys.path.insert(0, str((path / MAIN_FILENAME).parent)) spec.loader.exec_module(project_module) return project_module - def validate_url(url: str) -> Tuple[bool, str]: - """ - Validates a URL and returns a tuple of (is_valid, error_message). - - Args: - url (str): The URL to validate - - Returns: - Tuple[bool, str]: A tuple containing: - - Boolean indicating if the URL is valid - - Error message (empty string if valid) - """ - # Check if URL is empty + """Validates a URL and returns a tuple of (is_valid, error_message).""" if not url: return False, "URL cannot be empty" try: - # Parse the URL result = urlparse(url) - # Check for required components if not result.scheme: return False, "Missing protocol (e.g., http:// or https://)" if not result.netloc: return False, "Missing domain name" - # Validate scheme if result.scheme not in ['http', 'https']: return False, f"Invalid protocol: {result.scheme}" - # Basic domain validation if '.' not in result.netloc: return False, "Invalid domain format" @@ -180,15 +152,12 @@ def validate_url(url: str) -> Tuple[bool, str]: except Exception as e: return False, f"Invalid URL format: {str(e)}" - 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 + 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) else: - # This branch is used by Gunicorn print("Starting production server with Gunicorn") \ No newline at end of file From 5378071b94f824cfc192e821aef7d2179a78eb34 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 11 Feb 2025 21:15:29 -0800 Subject: [PATCH 34/40] return result --- .../src/main.py | 3 ++- agentstack/serve/Dockerfile | 4 ++-- agentstack/serve/serve.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/agentstack/frameworks/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py b/agentstack/frameworks/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py index c8c347d4..481f17e2 100644 --- a/agentstack/frameworks/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py +++ b/agentstack/frameworks/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py @@ -12,7 +12,8 @@ def run(): """ Run the agent. """ - instance.kickoff(inputs=agentstack.get_inputs()) + result = instance.kickoff(inputs=agentstack.get_inputs()) + return result def train(): diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index ae61cf9f..6c215f69 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -15,7 +15,7 @@ COPY . . RUN uv pip install --system psutil RUN uv pip install --system flask RUN uv pip install --system gunicorn -RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command # cache buster 2 +RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command # 1 RUN mkdir -p src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/gunicorn.config.py ./src @@ -26,4 +26,4 @@ RUN rm -rf /root/.cache/uv/* /root/.cache/pip/* /tmp/* EXPOSE 6969 # Use Gunicorn with config file -CMD ["gunicorn", "--config", "gunicorn.conf.py", "src.serve:app"] \ No newline at end of file +CMD ["gunicorn", "--config", "src/gunicorn.config.py", "src.serve:app"] \ No newline at end of file diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index 4a77c39c..73aa61fd 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -109,7 +109,7 @@ def run_project(command: str = 'run', api_args: Optional[Dict[str, str]] = None, try: log.notify("Running your agent...") project_main = _import_project_module(conf.PATH) - getattr(project_main, command)() + return getattr(project_main, command)() except ImportError as e: raise ValidationError(f"Failed to import AgentStack project at: {conf.PATH.absolute()}\n{e}") except Exception as e: From abd383b16aff2f84d563e5f2ef0079c69983bb73 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 11 Feb 2025 21:36:54 -0800 Subject: [PATCH 35/40] return session_id --- .../src/main.py | 15 +++++++++++---- agentstack/serve/Dockerfile | 4 ++-- agentstack/serve/serve.py | 5 +++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/agentstack/frameworks/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py b/agentstack/frameworks/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py index 481f17e2..9c962769 100644 --- a/agentstack/frameworks/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py +++ b/agentstack/frameworks/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py @@ -4,16 +4,23 @@ import agentstack import agentops -agentops.init(default_tags=agentstack.get_tags()) +agentops.init(default_tags=agentstack.get_tags(), skip_auto_end_session=True, auto_start_session=False) instance = {{cookiecutter.project_metadata.project_name|replace('-', '')|replace('_', '')|capitalize}}Crew().crew() -def run(): +def run() -> [str, str]: """ Run the agent. + Returns: + A Tuple: (The output of running the agent, agentops session_id) """ - result = instance.kickoff(inputs=agentstack.get_inputs()) - return result + session = agentops.start_session() + try: + result = instance.kickoff(inputs=agentstack.get_inputs()) + session.end_session(end_state="Success") + return result.raw, session.session_id + except: + session.end_session(end_state="Fail") def train(): diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index 6c215f69..da4bdbd3 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -9,13 +9,13 @@ RUN apt clean && rm -rf /var/lib/apt/lists/* # Install uv RUN pip install --no-cache-dir uv -# Copy everything since we're installing local package +# Copy everything since we're installing local package 1 COPY . . RUN uv pip install --system psutil RUN uv pip install --system flask RUN uv pip install --system gunicorn -RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command # 1 +RUN uv pip install --system git+https://github.com/AgentOps-AI/AgentStack.git@deploy-command RUN mkdir -p src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/serve.py ./src RUN cp /usr/local/lib/python3.12/site-packages/agentstack/serve/gunicorn.config.py ./src diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index 73aa61fd..c7248f34 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -69,10 +69,11 @@ def process_agent(): finally: if current_webhook_url: try: - result = run_project(api_inputs=request_data.get('inputs')) + result, session_id = run_project(api_inputs=request_data.get('inputs')) call_webhook(current_webhook_url, { 'status': 'success', - 'result': result + 'result': result, + 'session_id': session_id }) except Exception as e: error_message = str(e) From 318c4b80fcd16671736c434493ec957f69661790 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Tue, 11 Feb 2025 21:42:50 -0800 Subject: [PATCH 36/40] guid to string --- .../{{cookiecutter.project_metadata.project_slug}}/src/main.py | 2 +- agentstack/serve/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/agentstack/frameworks/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py b/agentstack/frameworks/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py index 9c962769..6f6d3dfc 100644 --- a/agentstack/frameworks/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py +++ b/agentstack/frameworks/templates/crewai/{{cookiecutter.project_metadata.project_slug}}/src/main.py @@ -18,7 +18,7 @@ def run() -> [str, str]: try: result = instance.kickoff(inputs=agentstack.get_inputs()) session.end_session(end_state="Success") - return result.raw, session.session_id + return result.raw, str(session.session_id) except: session.end_session(end_state="Fail") diff --git a/agentstack/serve/Dockerfile b/agentstack/serve/Dockerfile index da4bdbd3..3bcf8028 100644 --- a/agentstack/serve/Dockerfile +++ b/agentstack/serve/Dockerfile @@ -9,7 +9,7 @@ RUN apt clean && rm -rf /var/lib/apt/lists/* # Install uv RUN pip install --no-cache-dir uv -# Copy everything since we're installing local package 1 +# Copy everything since we're installing local package 3 COPY . . RUN uv pip install --system psutil From e4991eb8736f1b1d56d4aedfa4a69f47240b9acf Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Wed, 19 Feb 2025 17:06:10 -0800 Subject: [PATCH 37/40] use waitress --- agentstack/deploy.py | 13 +++++++++---- agentstack/serve/serve.py | 18 +++++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/agentstack/deploy.py b/agentstack/deploy.py index fe0e17d3..0deec179 100644 --- a/agentstack/deploy.py +++ b/agentstack/deploy.py @@ -17,8 +17,13 @@ import websockets +ORIGIN = "localhost:3000" +# ORIGIN = "host-production-ab3c.up.railway.app" +PROTOCOL = "http" +# PROTOCOL = "https" # "http" + async def connect_websocket(project_id, spinner): - uri = f"ws://localhost:3000/ws/build/{project_id}" + uri = f"ws://{ORIGIN}/ws/build/{project_id}" async with websockets.connect(uri) as websocket: try: while True: @@ -68,7 +73,7 @@ async def deploy(): spinner.update_message("Uploading to server") response = requests.post( - 'http://localhost:3000/deploy/build', + f'{PROTOCOL}://{ORIGIN}/deploy/build', files={'code': ('code.zip', zip_file)}, params={'projectId': project_id}, headers={'Authorization': f'Bearer {bearer_token}'} @@ -85,7 +90,7 @@ async def deploy(): await websocket_task log.success("\nš Successfully deployed with AgentStack.sh! Opening in browser...") - # webbrowser.open(f"http://localhost:5173/project/{project_id}") + # webbrowser.open(f"http://localhost:5173/project/{project_id}") # TODO: agentops platform url except Exception as e: spinner.stop() @@ -120,7 +125,7 @@ def get_project_id(): try: response = requests.post( - url="http://localhost:3000/projects", + url=f"{PROTOCOL}://{ORIGIN}/projects", # url="https://api.agentstack.sh/projects", headers=headers, json=payload diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index c7248f34..1af36f37 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -13,6 +13,7 @@ import requests from typing import Dict, Any, Optional, Tuple import os +from waitress import serve MAIN_FILENAME: Path = Path("src/main.py") MAIN_MODULE_NAME = "main" @@ -153,12 +154,27 @@ def validate_url(url: str) -> Tuple[bool, str]: except Exception as e: return False, f"Invalid URL format: {str(e)}" + +def get_waitress_config(): + return { + 'host': '0.0.0.0', + 'port': int(os.getenv('PORT') or '6969'), + 'threads': 1, # Similar to Gunicorn threads + 'connection_limit': 1, # Similar to worker_connections + 'channel_timeout': 300, # Similar to timeout + 'cleanup_interval': 2, # Similar to keepalive + 'log_socket_errors': True, + 'max_request_body_size': 1073741824, # 1GB + 'clear_untrusted_proxy_headers': True + } + 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) + # app.run(host='0.0.0.0', port=port) + serve(app, **get_waitress_config()) else: print("Starting production server with Gunicorn") \ No newline at end of file From 94ee47fc820327f4b7b78c8f0e7be11709d54df0 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Fri, 21 Feb 2025 11:26:52 -0800 Subject: [PATCH 38/40] use hosted --- agentstack/deploy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/agentstack/deploy.py b/agentstack/deploy.py index 0deec179..f22530d1 100644 --- a/agentstack/deploy.py +++ b/agentstack/deploy.py @@ -17,10 +17,10 @@ import websockets -ORIGIN = "localhost:3000" -# ORIGIN = "host-production-ab3c.up.railway.app" -PROTOCOL = "http" -# PROTOCOL = "https" # "http" +# ORIGIN = "localhost:3000" +ORIGIN = "build.agentstack.sh" +# PROTOCOL = "http" +PROTOCOL = "https" # "http" async def connect_websocket(project_id, spinner): uri = f"ws://{ORIGIN}/ws/build/{project_id}" From 9c96e45514720d20f7402e8f7ad0a836aa365a2a Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Fri, 21 Feb 2025 16:02:05 -0800 Subject: [PATCH 39/40] remove waitress import --- agentstack/serve/serve.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/agentstack/serve/serve.py b/agentstack/serve/serve.py index 1af36f37..aac6bcc5 100644 --- a/agentstack/serve/serve.py +++ b/agentstack/serve/serve.py @@ -13,7 +13,6 @@ import requests from typing import Dict, Any, Optional, Tuple import os -from waitress import serve MAIN_FILENAME: Path = Path("src/main.py") MAIN_MODULE_NAME = "main" @@ -174,7 +173,6 @@ def get_waitress_config(): 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) - serve(app, **get_waitress_config()) + app.run(host='0.0.0.0', port=port) else: print("Starting production server with Gunicorn") \ No newline at end of file From f8e69e47d5875081623c9f76711c39e53cb385e7 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton <bboynton97@gmail.com> Date: Fri, 21 Feb 2025 17:42:54 -0800 Subject: [PATCH 40/40] end deploy logs on finish deploy --- agentstack/deploy.py | 2 ++ pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/agentstack/deploy.py b/agentstack/deploy.py index f22530d1..030af0dc 100644 --- a/agentstack/deploy.py +++ b/agentstack/deploy.py @@ -39,6 +39,8 @@ async def connect_websocket(project_id, spinner): spinner.clear_and_log(f"š {data.get('data','')}", 'info') elif data['type'] == 'error': raise Exception(f"Failed to deploy: {data.get('data')}") + elif data['type'] == 'complete': + return except websockets.ConnectionClosed: raise Exception("Websocket connection closed unexpectedly") diff --git a/pyproject.toml b/pyproject.toml index 697d2c29..7ed57b34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentstack" -version = "0.3.3" +version = "0.3.5" description = "The fastest way to build robust AI agents" authors = [ { name="Braelyn Boynton", email="bboynton97@gmail.com" },