Skip to content
Draft
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
24 changes: 24 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ services:
required: false
environment:
- ENVIRONMENT=${ENVIRONMENT:-production}
- AI_SERVICE_URL=http://redbox_ai_service:8001
volumes:
- /app/django_app/frontend/node_modules
- ./django_app:/app/django_app/
Expand Down Expand Up @@ -175,6 +176,29 @@ services:
condition: service_healthy
restart: unless-stopped

redbox_ai_service:
build:
context: .
dockerfile: redbox_ai_service/Dockerfile
container_name: redbox_ai_service
ports:
- "8001:8001"
networks:
- redbox-app-network
env_file:
- path: tests/.env.integration
required: false
- path: .env
required: false
environment:
- ENV=local
depends_on:
redbox-django-app:
condition: service_healthy
mem_limit: 12g
cpus: 3
restart: unless-stopped

networks:
redbox-app-network:
driver: bridge
Expand Down
79 changes: 64 additions & 15 deletions redbox/redbox/graph/agents/workers.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,57 @@
import httpx
import logging

from json import JSONDecodeError

from langchain_core.runnables import RunnableLambda, RunnableParallel

from redbox.chains.parser import ClaudeParser
from redbox.chains.runnables import create_chain_agent
from redbox.graph.agents.base import Agent
from redbox.graph.agents.configs import AgentConfig
from redbox.graph.nodes.processes import build_activity_log_node
from redbox.graph.nodes.sends import run_tools_parallel
from redbox.models.chain import RedboxState, configure_agent_task_plan
from redbox.models.graph import RedboxActivityEvent
from redbox.transform import join_result_with_token_limit
from redbox.models.settings import get_settings

settings = get_settings()


class RemoteWorkerAgent(Agent):
"""
For the FastAPI service.
"""

def __init__(self, agent_name: str, base_url: str):
self.agent_name = agent_name
self.base_url = base_url.rstrip("/")
self.logger = logging.getLogger(f"RemoteWorkerAgent[{agent_name}]")

def invoke(self, state: RedboxState, task=None):
"""
Invokes remote agent.
`task` should be the task object containing `task.task` and `task.expected_output`
"""
url = f"{self.base_url}/invoke/{self.agent_name}"

payload = state.model_dump()
if task:
payload["_task_info"] = {
"task": task.task,
"expected_output": task.expected_output,
}

self.logger.warning(f"Invoking remote agent {self.agent_name} at {url}")
try:
response = httpx.post(url, json=payload, timeout=60)
response.raise_for_status()
result = response.json()
self.logger.warning(f"Remote agent {self.agent_name} returned successfully")
return result
except httpx.HTTPError as e:
self.logger.exception(f"Remote agent call failed: {e}")
raise RuntimeError(f"Remote agent call failed: {e}") from e


class WorkerAgent(Agent):
Expand Down Expand Up @@ -92,26 +133,34 @@ def core_task(self):
@RunnableLambda
def _core_task(input):
state, task = input
worker_agent = create_chain_agent(
system_prompt=self.config.prompt.get_prompt,
use_metadata=self.config.prompt.prompt_vars.metadata,
using_chat_history=self.config.prompt.prompt_vars.chat_history,
parser=self.config.parser,
tools=self.config.tools,
_additional_variables={"task": task.task, "expected_output": task.expected_output},
model=self.config.llm_backend,
use_knowledge_base=self.config.prompt.prompt_vars.knowledge_base_metadata,
)
# worker_agent = llm_call(agent_config=self.config)
self.logger.warning(f"[{self.config.name}] Invoking worker agent...")
ai_msg = worker_agent.invoke(state)

self.logger.warning(f"[{self.config.name}] Preparing worker agent...")

# if getattr(self.config, "use_remote_agent", False):
from redbox.models.settings import get_settings

settings = get_settings()
worker_agent = RemoteWorkerAgent(agent_name=self.config.name, base_url=settings.ai_service_url)
ai_msg = worker_agent.invoke(state, task=task)
# else:
# worker_agent = create_chain_agent(
# system_prompt=self.config.prompt.get_prompt,
# use_metadata=self.config.prompt.prompt_vars.metadata,
# using_chat_history=self.config.prompt.prompt_vars.chat_history,
# parser=self.config.parser,
# tools=self.config.tools,
# _additional_variables={"task": task.task, "expected_output": task.expected_output},
# model=self.config.llm_backend,
# use_knowledge_base=self.config.prompt.prompt_vars.knowledge_base_metadata,
# )
# ai_msg = worker_agent.invoke(state)

self.logger.warning(f"[{self.config.name}] Worker agent output:\n{ai_msg}")

# --- RUN TOOLS IN PARALLEL ---
self.logger.warning(f"[{self.config.name}] Running tools via run_tools_parallel...")

result = run_tools_parallel(ai_msg, self.config.tools, state)

return (state, result, task)

return _core_task.with_retry(stop_after_attempt=3)
Expand Down
2 changes: 2 additions & 0 deletions redbox/redbox/models/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ class Settings(BaseSettings):

max_attempts: int = os.environ.get("MAX_ATTEMPTS", 3)

ai_service_url: str = os.environ.get("AI_SERVICE_URL", "")

# mcp
caddy_mcp: MCPServerSettings = MCPServerSettings(
name="caddy_mcp",
Expand Down
33 changes: 33 additions & 0 deletions redbox_ai_service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
FROM python:3.12-slim

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
curl \
&& rm -rf /var/lib/apt/lists/*

RUN pip install --no-cache-dir poetry poetry-plugin-bundle

COPY redbox_ai_service/pyproject.toml redbox_ai_service/poetry.lock ./redbox_ai_service/
COPY redbox/pyproject.toml redbox/poetry.lock ./redbox/

COPY django_app ./django_app
COPY redbox ./redbox

COPY redbox_ai_service ./redbox_ai_service
COPY README.md ./README.md

WORKDIR /app/redbox_ai_service

ENV POETRY_VIRTUALENVS_IN_PROJECT=true \
POETRY_NO_INTERACTION=1 \
PYTHONPATH=/app

RUN poetry install --no-interaction --no-ansi

ENV PATH="/app/redbox_ai_service/.venv/bin:$PATH"

EXPOSE 8001

CMD ["uvicorn", "redbox_ai_service.main:app", "--host", "0.0.0.0", "--port", "8001"]
Empty file added redbox_ai_service/README.md
Empty file.
Empty file added redbox_ai_service/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions redbox_ai_service/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from redbox.graph.agents.configs import agent_configs, AgentConfig


def get_agent_config(name: str) -> AgentConfig:
"""
Fetch one of the pre-defined AgentConfig objects
"""
if name not in agent_configs:
raise KeyError(f"Agent {name} not found")

return agent_configs[name]
35 changes: 35 additions & 0 deletions redbox_ai_service/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from fastapi import FastAPI, HTTPException
from redbox.models.chain import RedboxState
from redbox.graph.agents.workers import WorkerAgent
from redbox_ai_service.config import get_agent_config

app = FastAPI(title="Redbox AI Service")


@app.post("/invoke/{agent_name}")
def invoke(agent_name: str, payload: dict):
"""
Invoke a specific worker agent by name - gets payload of RedboxState
If _task_info exists, then it puts it into execution
"""
try:
state = RedboxState(**payload)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Invalid state: {e}")

try:
config = get_agent_config(agent_name)
except KeyError:
raise HTTPException(status_code=404, detail=f"Unknown agent {agent_name}")

task_info = payload.get("_task_info")
if task_info:
state._task_info = task_info

agent = WorkerAgent(config)
try:
result = agent.execute().invoke(state)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Agent execution failed: {e}")

return result
Loading
Loading