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
48 changes: 27 additions & 21 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,46 +1,52 @@
# ---------------------------------------------------------
# Misc.
# ---------------------------------------------------------

# Set the default goal.
.DEFAULT_GOAL := build

# Tell Docker to build images in parallel.
COMPOSE_BAKE := true

# Set the environment variables file to ".env" if an argument is not provided.
ENV_FILE ?= .env
include $(ENV_FILE)


# Set the Docker Compose profile to "all" if an argument is not provided.
DOCKER_COMPOSE_PROFILE ?= all

# ---------------------------------------------------------
# Build the containers.
# ---------------------------------------------------------

.PHONY: build
.SILENT: build

build:
docker compose --profile $(DOCKER_COMPOSE_PROFILE) --env-file $(ENV_FILE) build --no-cache

build:
docker compose --profile $(DOCKER_COMPOSE_PROFILE) build
# ---------------------------------------------------------
# Start the containers.
# ---------------------------------------------------------

.PHONY: start
.SILENT: start

start:
docker compose --profile $(DOCKER_COMPOSE_PROFILE) --env-file $(ENV_FILE) up -d

docker compose --profile $(DOCKER_COMPOSE_PROFILE) up -d
# ---------------------------------------------------------
# Stop the containers.
# ---------------------------------------------------------

.PHONY: stop
.SILENT: stop

stop:
docker compose --profile $(DOCKER_COMPOSE_PROFILE) --env-file $(ENV_FILE) down

stop:
docker compose --profile $(DOCKER_COMPOSE_PROFILE) down

# ---------------------------------------------------------
# Test the agent.
# ---------------------------------------------------------

.PHONY: test/agent
.SILENT: test/agent

test/agent:
curl -X POST localhost:8181/api/v1/ -d '{"message":"hello"}'; echo
14 changes: 14 additions & 0 deletions agent/aoai_gpt-4o/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM alpine:3.23
LABEL image.authors="Vic Fernandez III <@cyberphor>"
WORKDIR /home/swordfish/
COPY requirements.txt requirements.txt
RUN apk add --no-cache --update python3 py3-pip uvicorn &&\
pip install --break-system-packages -r requirements.txt &&\
adduser -D swordfish -h /home/swordfish
COPY entrypoint.sh entrypoint.sh
COPY swordfish/ swordfish/
RUN chown -R swordfish:swordfish /home/swordfish &&\
chmod +x entrypoint.sh
USER swordfish
EXPOSE 8181
ENTRYPOINT [ "./entrypoint.sh" ]
4 changes: 4 additions & 0 deletions agent/aoai_gpt-4o/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh

set -e
uvicorn swordfish.main:api --host 0.0.0.0 --port 8181
4 changes: 4 additions & 0 deletions agent/aoai_gpt-4o/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
azure-identity
fastapi
openai
openai-agents
Empty file.
49 changes: 49 additions & 0 deletions agent/aoai_gpt-4o/swordfish/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from os import getenv

from agents import Agent, OpenAIChatCompletionsModel, Runner, set_tracing_disabled
from agents.mcp import MCPServerStreamableHttp
from azure.identity import (
AzureAuthorityHosts,
DefaultAzureCredential,
get_bearer_token_provider,
)
from openai import AsyncAzureOpenAI
from fastapi import FastAPI, Request

AZURE_TOKEN_SCOPES = getenv("AZURE_TOKEN_SCOPES")
AZURE_OPENAI_ENDPOINT = getenv("AZURE_OPENAI_ENDPOINT")
AZURE_OPENAI_API_VERSION = getenv("AZURE_OPENAI_API_VERSION")
MCP_SERVER_ENDPOINT = getenv("MCP_SERVER_ENDPOINT")
AZURE_OPENAI_DEPLOYMENT = getenv("AZURE_OPENAI_DEPLOYMENT")

token_provider = get_bearer_token_provider(
DefaultAzureCredential(authority=AzureAuthorityHosts.AZURE_GOVERNMENT),
AZURE_TOKEN_SCOPES
)

client = AsyncAzureOpenAI(
azure_endpoint=AZURE_OPENAI_ENDPOINT,
api_version=AZURE_OPENAI_API_VERSION,
azure_ad_token_provider=token_provider
)
set_tracing_disabled(disabled=True)

api = FastAPI()
history = []

@api.post("/api/v1/")
async def respond(request: Request):
global history
request = await request.json()
async with MCPServerStreamableHttp(name="Swordfish Tools", params={"url": MCP_SERVER_ENDPOINT}, cache_tools_list=True) as mcp_server:
model = OpenAIChatCompletionsModel(
model=AZURE_OPENAI_DEPLOYMENT,
openai_client=client,
)
agent = Agent(
name="swordfish",
model=model,
mcp_servers=[mcp_server],
)
result = await Runner.run(starting_agent=agent, input=request["message"])
return result.final_output
5 changes: 4 additions & 1 deletion agent/openai_gpt-4o/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
fastapi
openai
mcp
strands-agents
psycopg
psycopg[pool]
psycopg[binary]
strands-agents
22 changes: 22 additions & 0 deletions agent/openai_gpt-4o/swordfish/history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# agent/swordfish/history.py

from abc import ABC, abstractmethod

class HistoryStore(ABC):
@abstractmethod
async def get(self, session_id: str) -> list:
pass

@abstractmethod
async def save(self, session_id: str, messages: list):
pass

class InMemoryStore(HistoryStore):
def __init__(self):
self.storage = {}

async def get(self, session_id: str):
return self.storage.get(session_id, [])

async def save(self, session_id: str, messages: list):
self.storage[session_id] = messages
78 changes: 61 additions & 17 deletions agent/openai_gpt-4o/swordfish/main.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,80 @@
# Standard-library imports.
import json
from os import environ
from contextlib import asynccontextmanager

# Third-party imports.
from fastapi import FastAPI, Request
from mcp.client.sse import sse_client
from psycopg_pool import AsyncConnectionPool

from strands import Agent
from strands.models.openai import OpenAIModel
from strands.tools.mcp import MCPClient

api = FastAPI()
from .history import HistoryStore, InMemoryStore


class PostgresStore(HistoryStore):
def __init__(self, pool: AsyncConnectionPool):
self.pool = pool

async def get(self, session_id: str) -> list:
async with self.pool.connection() as conn:
async with conn.cursor() as cur:
await cur.execute("SELECT messages FROM chat_sessions WHERE session_id = %s", (session_id,))
result = await cur.fetchone()
return result[0] if result else []

async def save(self, session_id: str, messages: list):
async with self.pool.connection() as conn:
async with conn.cursor() as cur:
await cur.execute("""
INSERT INTO chat_sessions (session_id, messages)
VALUES (%s, %s)
ON CONFLICT (session_id) DO UPDATE SET messages = EXCLUDED.messages
""", (session_id, json.dumps(messages)))

@asynccontextmanager
async def lifespan(app: FastAPI):
store_type = environ.get("STORE_TYPE", "memory").lower()

match store_type:
case "postgres":
db_url = environ["DATABASE_URL"]
pool = AsyncConnectionPool(conninfo=db_url, open=False)
await pool.open()
app.state.history = PostgresStore(pool)
yield
await pool.close()
case _:
app.state.history = InMemoryStore()
yield

api = FastAPI(lifespan=lifespan)
sse_mcp_client = MCPClient(lambda: sse_client("http://swordfish-tools:8282/sse"))
history = []

@api.post("/api/v1/")
async def respond(request: Request):
global history
request = await request.json()
store: HistoryStore = request.app.state.history

body = await request.json()
session_id = body.get("session_id", "default_session")
user_message = body.get("message")

current_history = await store.get(session_id)

with sse_mcp_client:
model = OpenAIModel(
client_args={
"api_key": environ["OPENAI_API_KEY"],
},
model_id=request.get("model", "gpt-4o"),
params={
"max_tokens": 1000,
"temperature": 0.7,
},
client_args={"api_key": environ["OPENAI_API_KEY"]},
model_id=body.get("model", "gpt-4o"),
params={"max_tokens": 1000, "temperature": 0.7},
)

tools = sse_mcp_client.list_tools_sync()

agent = Agent(model=model, tools=tools)
agent.messages = history
response = agent(prompt=request["message"])
history = agent.messages
agent.messages = current_history
response = agent(prompt=user_message)

await store.save(session_id, agent.messages)

return response.message["content"][0]["text"]
22 changes: 8 additions & 14 deletions compose.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
services:

docs:
profiles: [ all, docs ]
profiles: [ docs ]
build: docs
image: swordfish/docs:latest
container_name: swordfish-docs
ports:
- "5050:5050"
env_file:
- .env

emass:
profiles: [ all, emass ]
Expand All @@ -17,9 +15,7 @@ services:
container_name: swordfish-emass
ports:
- "4010:4010"
env_file:
- .env


tools:
profiles: [ all, tools ]
build: tools
Expand All @@ -28,24 +24,22 @@ services:
ports:
- "8282:8282"
env_file:
- .env

- tools/.env
agent:
profiles: [ all, agent ]
build: agent/openai_gpt-4o
build: agent/aoai_gpt-4o
image: swordfish/agent:latest
container_name: swordfish-agent
ports:
- "8181:8181"
env_file:
- .env

- agent/aoai_gpt-4o/.env
frontend:
profiles: [ all, frontend ]
build: frontend
image: swordfish/frontend:latest
container_name: swordfish-frontend
ports:
- "80:8501"
env_file:
- .env
16 changes: 16 additions & 0 deletions docs/swordfish/developer-guide/agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Agents

## Azure GPT-4o
```bash
export AZURE_TENANT_ID="<INSERT_VALUE>"
export AZURE_CLIENT_ID="<INSERT_VALUE>"
export AZURE_CLIENT_SECRET="<INSERT_VALUE>"

export AZURE_CLOUD="<INSERT_VALUE>"
export AZURE_TOKEN_SCOPES="https://cognitiveservices.azure.us/.default"

export AZURE_OPENAI_ENDPOINT="https://<INSERT_VALUE>.openai.azure.us/"
export AZURE_OPENAI_API_VERSION="<INSERT_VALUE>"
export AZURE_OPENAI_DEPLOYMENT="swordfish"
export MCP_SERVER_ENDPOINT="http://swordfish-tools:8282/sse"
```
Loading
Loading