Skip to content

Commit 02edff4

Browse files
committed
Merge branch 'main' into feat_add_human_in_the_loop_agent
# Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit.
2 parents 3372b8c + 5299539 commit 02edff4

53 files changed

Lines changed: 4086 additions & 41 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agents/crewai/websearch_agent/Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ COPY src/ ./src/
1919
# Install the project and its dependencies using uv
2020
RUN uv pip install --system --no-cache .
2121

22-
# Copy the application entrypoint
22+
# Copy the application entrypoint, playground UI, and images
2323
COPY main.py .
24+
COPY playground/ ./playground/
25+
COPY images/ ./images/
2426

2527
# Pre-create the directory that CrewAI writes to at import time
2628
# (crewai.utilities.paths.db_storage_path -> ~/.local/share/app/)

agents/crewai/websearch_agent/README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,52 @@ curl -sN -X POST https://<YOUR_ROUTE_URL>/chat/completions \
201201

202202
---
203203

204+
## Playground UI
205+
206+
A browser-based chat interface is served directly by the agent at the root URL — no separate process needed.
207+
208+
### Running the Playground
209+
210+
Start the agent and open the root URL in your browser:
211+
212+
```bash
213+
uvicorn main:app --port 8000
214+
```
215+
216+
Open [http://localhost:8000](http://localhost:8000) in your browser.
217+
218+
A green dot in the header means the agent is connected and ready. Type a message and press **Enter** to send.
219+
220+
When deployed to OpenShift, the playground is available at the route URL.
221+
222+
### Standalone Flask Playground (alternative)
223+
224+
You can also run the playground as a separate Flask app if needed:
225+
226+
```bash
227+
uv pip install flask
228+
```
229+
230+
```bash
231+
# Terminal 1: Start the agent
232+
uvicorn main:app --port 8000
233+
234+
# Terminal 2: Start the playground
235+
flask --app playground/app run --port 5001
236+
```
237+
238+
| Variable | Default | Description |
239+
|-------------|--------------------------|---------------------------------|
240+
| `AGENT_URL` | `http://localhost:8000` | URL of the running agent API |
241+
242+
If the agent runs on a different host or port:
243+
244+
```bash
245+
AGENT_URL=https://your-agent-url flask --app playground/app run --port 5001
246+
```
247+
248+
---
249+
204250
## Agent-Specific Documentation
205251

206252
- [CrewAI Documentation](https://docs.crewai.com/)

agents/crewai/websearch_agent/deploy.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ set -e # Exit on error
1616
source .env
1717
export CONTAINER_IMAGE BASE_URL MODEL_ID
1818

19+
## ============================================
20+
# COPY SHARED IMAGES FOR DOCKER BUILD CONTEXT
21+
## ============================================
22+
23+
cp -r ../../../images ./images && echo "Images copied into build context"
24+
trap 'rm -rf ./images' EXIT
25+
1926
## ============================================
2027
# DOCKER BUILD
2128
## ============================================

agents/crewai/websearch_agent/examples/ai_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from os import getenv
2+
13
from crewai import LLM
24
from crewai.agents.parser import AgentAction, AgentFinish
35
from crewai.tools.tool_types import ToolResult
@@ -20,8 +22,6 @@ def ai_stream_service(context, base_url=None, model_id=None):
2022
Returns:
2123
Tuple (generate, generate_stream).
2224
"""
23-
from os import getenv
24-
2525
api_key = getenv("API_KEY", "no-key")
2626

2727
if base_url and not base_url.endswith("/v1"):

agents/crewai/websearch_agent/init.sh

100644100755
File mode changed.

agents/crewai/websearch_agent/main.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
import uuid
77
from contextlib import asynccontextmanager
88
from os import getenv
9+
from pathlib import Path
910

1011
from crewai import LLM
1112
from fastapi import FastAPI, HTTPException
12-
from fastapi.responses import StreamingResponse
13+
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
1314
from pydantic import BaseModel, Field
1415

1516
from crewai_web_search.crew import AssistanceAgents
@@ -364,6 +365,35 @@ async def health():
364365
return {"status": "healthy", "agent_initialized": llm is not None}
365366

366367

368+
# ── Playground UI ────────────────────────────────────────────────────────────
369+
_BASE_DIR = Path(__file__).resolve().parent
370+
_PLAYGROUND_HTML = _BASE_DIR / "playground" / "templates" / "index.html"
371+
# In Docker the images are copied to /app/images; locally they live at the repo root
372+
_IMAGES_DIR = _BASE_DIR / "images"
373+
if not _IMAGES_DIR.is_dir():
374+
_IMAGES_DIR = _BASE_DIR.parent.parent.parent / "images"
375+
376+
377+
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
378+
async def playground():
379+
"""Serve the playground chat UI."""
380+
return FileResponse(_PLAYGROUND_HTML)
381+
382+
383+
@app.get("/images/{filename:path}", include_in_schema=False)
384+
async def serve_image(filename: str):
385+
"""Serve images from the project-level images directory."""
386+
base = _IMAGES_DIR.resolve()
387+
file_path = (base / filename).resolve()
388+
try:
389+
file_path.relative_to(base)
390+
except ValueError:
391+
raise HTTPException(status_code=404, detail="Image not found")
392+
if not file_path.is_file():
393+
raise HTTPException(status_code=404, detail="Image not found")
394+
return FileResponse(file_path)
395+
396+
367397
if __name__ == "__main__":
368398
import uvicorn
369399

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""
2+
Playground UI for the CrewAI Web Search Agent.
3+
4+
A simple Flask chat interface that proxies requests to the agent's
5+
/chat/completions endpoint with streaming support.
6+
7+
Usage:
8+
# Make sure the agent is running first (default: http://localhost:8000)
9+
cd agents/crewai/websearch_agent
10+
flask --app playground/app run --port 5001
11+
12+
# Or with a custom agent URL:
13+
AGENT_URL=http://localhost:8000 flask --app playground/app run --port 5001
14+
"""
15+
16+
import json
17+
import logging
18+
from os import getenv
19+
from pathlib import Path
20+
21+
import requests as http_requests
22+
from flask import (
23+
Flask,
24+
Response,
25+
jsonify,
26+
render_template,
27+
request,
28+
send_from_directory,
29+
stream_with_context,
30+
)
31+
32+
logging.basicConfig(level=logging.DEBUG)
33+
logger = logging.getLogger(__name__)
34+
35+
IMAGES_DIR = Path(__file__).resolve().parents[4] / "images"
36+
37+
app = Flask(__name__)
38+
39+
40+
@app.route("/images/<path:filename>")
41+
def serve_image(filename):
42+
"""Serve images from the project-level images directory."""
43+
return send_from_directory(IMAGES_DIR, filename)
44+
45+
46+
AGENT_URL = getenv("AGENT_URL", "http://localhost:8000")
47+
48+
49+
@app.route("/")
50+
def index():
51+
return render_template("index.html")
52+
53+
54+
@app.route("/api/health", methods=["GET"])
55+
def health():
56+
"""Check if the agent is reachable."""
57+
try:
58+
resp = http_requests.get(f"{AGENT_URL}/health", timeout=5)
59+
return jsonify(resp.json()), resp.status_code
60+
except Exception:
61+
logger.exception("Error checking agent health")
62+
return (
63+
jsonify(
64+
{
65+
"status": "unreachable",
66+
"error": "Agent is unreachable. Please try again later.",
67+
}
68+
),
69+
503,
70+
)
71+
72+
73+
@app.route("/api/chat", methods=["POST"])
74+
def chat():
75+
"""Proxy chat requests to the agent with streaming."""
76+
data = request.get_json() or {}
77+
messages = data.get("messages", [])
78+
79+
payload = {
80+
"messages": messages,
81+
"stream": True,
82+
}
83+
84+
logger.info(f"Sending request to {AGENT_URL}/chat/completions (messages={len(payload.get('messages', []))}, stream={payload.get('stream')})")
85+
86+
def generate():
87+
try:
88+
with http_requests.post(
89+
f"{AGENT_URL}/chat/completions",
90+
json=payload,
91+
stream=True,
92+
timeout=(10, 300),
93+
) as resp:
94+
logger.info(f"Agent response status: {resp.status_code}")
95+
96+
if resp.status_code != 200:
97+
error_msg = resp.text[:500]
98+
logger.error(f"Agent error: {error_msg}")
99+
error = json.dumps(
100+
{
101+
"error": {
102+
"message": f"Agent returned {resp.status_code}: {error_msg}"
103+
}
104+
}
105+
)
106+
yield f"data: {error}\n\n"
107+
return
108+
109+
for chunk in resp.iter_content(chunk_size=None, decode_unicode=True):
110+
if chunk:
111+
logger.debug(f"Chunk: {chunk[:200]}")
112+
yield chunk
113+
114+
except http_requests.exceptions.ConnectionError:
115+
logger.error(f"Cannot connect to agent at {AGENT_URL}")
116+
error = json.dumps(
117+
{
118+
"error": {
119+
"message": f"Cannot connect to agent at {AGENT_URL}. Is it running?"
120+
}
121+
}
122+
)
123+
yield f"data: {error}\n\n"
124+
except http_requests.exceptions.ReadTimeout:
125+
logger.error("Agent request timed out")
126+
error = json.dumps({"error": {"message": "Agent request timed out (300s)"}})
127+
yield f"data: {error}\n\n"
128+
except Exception:
129+
logger.exception("Unexpected error in proxy")
130+
error = json.dumps({"error": {"message": "Internal server error"}})
131+
yield f"data: {error}\n\n"
132+
133+
return Response(
134+
stream_with_context(generate()),
135+
content_type="text/event-stream",
136+
headers={
137+
"Cache-Control": "no-cache",
138+
"X-Accel-Buffering": "no",
139+
"Connection": "keep-alive",
140+
},
141+
)
142+
143+
144+
if __name__ == "__main__":
145+
debug_mode = getenv("FLASK_DEBUG", "false").lower() == "true"
146+
app.run(debug=debug_mode, port=5001)

0 commit comments

Comments
 (0)