Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions samples/agentic-strands/.devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

FROM mcr.microsoft.com/devcontainers/python:3.11-bookworm
11 changes: 11 additions & 0 deletions samples/agentic-strands/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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": {}
}
}
21 changes: 21 additions & 0 deletions samples/agentic-strands/.github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -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
53 changes: 53 additions & 0 deletions samples/agentic-strands/README.md
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions samples/agentic-strands/app/.dockerignore
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions samples/agentic-strands/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
__pycache__/
9 changes: 9 additions & 0 deletions samples/agentic-strands/app/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
1 change: 1 addition & 0 deletions samples/agentic-strands/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import agent
168 changes: 168 additions & 0 deletions samples/agentic-strands/app/agent.py
Original file line number Diff line number Diff line change
@@ -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(' ', '+')
Comment thread
commit111 marked this conversation as resolved.

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)
Loading
Loading