diff --git a/samples/agentic-strands/.devcontainer/Dockerfile b/samples/agentic-strands/.devcontainer/Dockerfile new file mode 100644 index 000000000..b08b78db5 --- /dev/null +++ b/samples/agentic-strands/.devcontainer/Dockerfile @@ -0,0 +1,2 @@ + +FROM mcr.microsoft.com/devcontainers/python:3.11-bookworm diff --git a/samples/agentic-strands/.devcontainer/devcontainer.json b/samples/agentic-strands/.devcontainer/devcontainer.json new file mode 100644 index 000000000..67cac5b2d --- /dev/null +++ b/samples/agentic-strands/.devcontainer/devcontainer.json @@ -0,0 +1,11 @@ +{ + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + "features": { + "ghcr.io/defanglabs/devcontainer-feature/defang-cli:1.0.4": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/aws-cli:1": {} + } +} \ No newline at end of file diff --git a/samples/agentic-strands/.github/workflows/deploy.yaml b/samples/agentic-strands/.github/workflows/deploy.yaml new file mode 100644 index 000000000..0f33856f0 --- /dev/null +++ b/samples/agentic-strands/.github/workflows/deploy.yaml @@ -0,0 +1,21 @@ +name: Deploy + +on: + push: + branches: + - main + +jobs: + deploy: + environment: playground + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Deploy + uses: DefangLabs/defang-github-action@v1.1.3 diff --git a/samples/agentic-strands/README.md b/samples/agentic-strands/README.md new file mode 100644 index 000000000..f5500a6ca --- /dev/null +++ b/samples/agentic-strands/README.md @@ -0,0 +1,53 @@ +# Agentic Strands + +[![1-click-deploy](https://raw.githubusercontent.com/DefangLabs/defang-assets/main/Logos/Buttons/SVG/deploy-with-defang.svg)](https://portal.defang.dev/redirect?url=https%3A%2F%2Fgithub.com%2Fnew%3Ftemplate_name%3Dsample-agentic-strands-template%26template_owner%3DDefangSamples) + +This sample demonstrates a Strands Agent application, deployed with Defang. This [Strands](https://strandsagents.com/latest/) Agent can use tools, and is compatible with the [Defang OpenAI Access Gateway](https://github.com/DefangLabs/openai-access-gateway/). + +## Prerequisites + +1. Download [Defang CLI](https://github.com/DefangLabs/defang) +2. (Optional) If you are using [Defang BYOC](https://docs.defang.io/docs/concepts/defang-byoc) authenticate with your cloud provider account +3. (Optional for local development) [Docker CLI](https://docs.docker.com/engine/install/) + +## Development + +To run the application locally, you can use the following command: + +```bash +docker compose -f compose.dev.yaml up --build +``` + +## Configuration + +For this sample, you will not need to provide any [configuration](https://docs.defang.io/docs/concepts/configuration). However, if you ever need to, below is an example of how to do so in Defang: + +```bash +defang config set API_KEY +``` + +## Deployment + +> [!NOTE] +> Download [Defang CLI](https://github.com/DefangLabs/defang) + +### Defang Playground + +Deploy your application to the Defang Playground by opening up your terminal and typing: +```bash +defang compose up +``` + +### BYOC + +If you want to deploy to your own cloud account, you can [use Defang BYOC](https://docs.defang.io/docs/tutorials/deploy-to-your-cloud). + +--- + +Title: Agentic Strands + +Short Description: A Strands Agent application, deployed with Defang. + +Tags: Python, Flask, Strands, AI, Agent + +Languages: Python diff --git a/samples/agentic-strands/app/.dockerignore b/samples/agentic-strands/app/.dockerignore new file mode 100644 index 000000000..74a7bf9e0 --- /dev/null +++ b/samples/agentic-strands/app/.dockerignore @@ -0,0 +1,27 @@ +# Default .dockerignore file for Defang +**/__pycache__ +**/.direnv +**/.DS_Store +**/.envrc +**/.git +**/.github +**/.idea +**/.next +**/.vscode +**/compose.*.yaml +**/compose.*.yml +**/compose.yaml +**/compose.yml +**/docker-compose.*.yaml +**/docker-compose.*.yml +**/docker-compose.yaml +**/docker-compose.yml +**/node_modules +**/Thumbs.db +Dockerfile +*.Dockerfile +# Ignore our own binary, but only in the root to avoid ignoring subfolders +defang +defang.exe +# Ignore our project-level state +.defang \ No newline at end of file diff --git a/samples/agentic-strands/app/.gitignore b/samples/agentic-strands/app/.gitignore new file mode 100644 index 000000000..d50a09fc9 --- /dev/null +++ b/samples/agentic-strands/app/.gitignore @@ -0,0 +1,2 @@ +.env +__pycache__/ diff --git a/samples/agentic-strands/app/Dockerfile b/samples/agentic-strands/app/Dockerfile new file mode 100644 index 000000000..ea1afc16b --- /dev/null +++ b/samples/agentic-strands/app/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY . /app + +RUN pip install --no-cache-dir -r requirements.txt + +CMD ["python", "agent.py"] diff --git a/samples/agentic-strands/app/__init__.py b/samples/agentic-strands/app/__init__.py new file mode 100644 index 000000000..02c597e11 --- /dev/null +++ b/samples/agentic-strands/app/__init__.py @@ -0,0 +1 @@ +from . import agent diff --git a/samples/agentic-strands/app/agent.py b/samples/agentic-strands/app/agent.py new file mode 100644 index 000000000..dc1490359 --- /dev/null +++ b/samples/agentic-strands/app/agent.py @@ -0,0 +1,168 @@ +from strands import Agent, tool +from strands.models.openai import OpenAIModel +from flask import Flask, request, jsonify, send_from_directory +import requests + +import os +import dotenv + +dotenv.load_dotenv() + +message = """ +You are a helpful library assistant. +Your goal is to help users discover books available through the library's book API, based on the user's preferences. +When a user makes a request, you should search the API and suggest books that match their query. + +When interacting, ask the user clear questions to guide the search. +Make sure to explicitly state the question you are asking, +and provide simple sample answers so the user knows what to type. +Keep it to a maximum of 3 simple questions. +""" + +app = Flask(__name__) +latest_response = {"message": "Hello! I'm your library assistant. How can I help you with your reading today?"} + +model = OpenAIModel( + client_args={ + "base_url": os.getenv("LLM_URL"), + # "api_key": os.getenv("OPENAI_API_KEY") + }, + model_id=os.getenv("LLM_MODEL"), + params={ + "max_tokens": 1000, + "temperature": 0.7, + } +) + +def parse_assistant_response(**kwargs): + # Extract the assistant's text message from JSON + assistant_text = kwargs["message"]["content"][0]["text"] + + print("Assistant Text: ", assistant_text) + return assistant_text + + +def message_buffer_handler(**kwargs): + # When a new message is created from the assistant, print its content + global latest_response + try: + if "message" in kwargs and kwargs["message"].get("role") == "assistant": + # Parse the assistant's response from JSON + assistant_text = parse_assistant_response(**kwargs) + + # Send the assistant's message content back to the UI + latest_response = {"message": assistant_text} + + # Prevent the agent from closing by not calling exit() or any termination logic here. + # If you have any cleanup or state reset, do it here, but do not terminate the process. + pass + + except Exception as e: + print(f"Error in message_buffer_handler: {str(e)}") + +@tool +def search_for_books(query, filters=None) -> str: + """ + Search for detailed information about books using the Open Library API. + + Args: + query: The search term to look up books. + + Returns: + A string summarizing the list of matching books, or a message if none are found. + """ + + # Replace spaces in the query with plus signs for URL encoding + clean_query = query.replace(' ', '+') + + url = f"https://openlibrary.org/search.json" + headers = {} + params = { + "q": clean_query, + "page": 1, + "limit": 10 + } + + try: + response = requests.get(url, headers=headers, params=params) + if response.ok: + book_list = response.json() + if book_list.get("num_found", 0) == 0: + return "No books found matching your query." + + message = "Here are the books I found:" + for book in book_list.get("docs", []): + title = book.get("title") + author = book.get("author_name", ["Unknown"])[0] + year = book.get("first_publish_year") + message += f"\n- Title: {title}, Author: {author}, Year: {year}" + print(message) + return message + else: + return f"Error: API request failed: {response.status_code}" + except Exception as e: + return f"Error: {str(e)}" + +TOOL_SPEC = { + "name": "search_for_books", + "description": "Get detailed information about books from Open Library, based on a search query.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query for books", + } + }, + "required": ["query"], + }, +} + +agent = Agent( + tools=[search_for_books], + model=model, + callback_handler=message_buffer_handler, + system_prompt=message +) + +print("Agent model:", agent.model.config) + +# Flask routes +@app.route('/') +def index(): + # This assumes index.html is in the same directory as this script + return send_from_directory('.', 'index.html') + +@app.route('/chat', methods=['POST']) +def chat(): + try: + global latest_response + data = request.json + if not data: + return jsonify({"error": "No JSON data received"}), 400 + + user_message = data.get('message') + if not user_message: + return jsonify({"error": "No message provided"}), 400 + print(f"Received message: {user_message}") + + agent(f"Continue the conversation with the user. The user says: {user_message}") + + response_content = latest_response.get("message", "I'm thinking about your question...") + + return jsonify({ + "response": response_content + }) + + except Exception as e: + print(f"Error in /chat endpoint: {str(e)}") + return jsonify({"error": str(e), "response": str(e)}), 500 + +# Start Flask server when this script is run directly +if __name__ == '__main__': + + print("Environment variables:") + print(f"- LLM_URL: {os.getenv('LLM_URL')}") + + print("Starting Flask server on port 5001") + app.run(host='0.0.0.0', port=5001, debug=False) diff --git a/samples/agentic-strands/app/index.html b/samples/agentic-strands/app/index.html new file mode 100644 index 000000000..c1d2aa993 --- /dev/null +++ b/samples/agentic-strands/app/index.html @@ -0,0 +1,170 @@ + + + + + Library Assistant AI + + + + +
+

Library Assistant AI

+

Chat with our AI Library Assistant to get personalized book recommendations.

+ +
+
+ Hello! I'm your library assistant. How can I help you with your reading today? +
+
+ +
+ + +
+ + +
+ + diff --git a/samples/agentic-strands/app/requirements.txt b/samples/agentic-strands/app/requirements.txt new file mode 100644 index 000000000..2a785c6f8 --- /dev/null +++ b/samples/agentic-strands/app/requirements.txt @@ -0,0 +1,7 @@ +strands-agents>=1.5.0 +strands-agents-tools>=0.2.0 +openai===1.99.9 +dotenv===0.9.9 +flask==2.0.1 +werkzeug==2.3.7 +requests==2.31.0 diff --git a/samples/agentic-strands/compose.dev.yaml b/samples/agentic-strands/compose.dev.yaml new file mode 100644 index 000000000..ba08b8f4d --- /dev/null +++ b/samples/agentic-strands/compose.dev.yaml @@ -0,0 +1,27 @@ +services: + app: + extends: + file: compose.yaml + service: app + ports: + - target: 5001 + published: 5001 + mode: ingress + + llm: + extends: + file: compose.yaml + service: llm + # AWS section + environment: + - AWS_REGION=${AWS_REGION} # replace with your AWS region + - AWS_PROFILE=${AWS_PROFILE} # replace with your AWS profile name + - PROVIDER=aws + volumes: + - type: bind + source: ~/.aws + target: /root/.aws + + # GCP section + # environment: + # - GCP_PROJECT_ID=${GCP_PROJECT_ID} # replace with your GCP project ID diff --git a/samples/agentic-strands/compose.yaml b/samples/agentic-strands/compose.yaml new file mode 100644 index 000000000..5ad75cc75 --- /dev/null +++ b/samples/agentic-strands/compose.yaml @@ -0,0 +1,33 @@ +services: + app: + # uncomment to add your own domain + # domainname: example.com + build: + context: ./app + dockerfile: Dockerfile + ports: + - target: 5001 + published: 5001 + mode: ingress + environment: + LLM_URL: http://llm/api/v1/ + LLM_MODEL: default + OPENAI_API_KEY: FAKE_TOKEN + depends_on: + - llm + + llm: + environment: + - OPENAI_API_KEY=FAKE_TOKEN + image: defangio/openai-access-gateway:06339c7 + ports: + - target: 80 + published: 80 + protocol: tcp + mode: host + x-defang-llm: true + deploy: + resources: + reservations: + cpus: 0.25 + memory: 512MB