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" },