diff --git a/.env.example b/.env.example index f5eebf6a..8351ad5b 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,6 @@ AUTH0_SECRET= AUTH0_BASE_URL=http://localhost:3000 AUTH0_ISSUER_BASE_URL= AUTH0_CLIENT_ID= -AUTH0_CLIENT_SECRET= \ No newline at end of file +AUTH0_CLIENT_SECRET= +COPILOT_API_KEY=test +AGENTS_API_KEY=test \ No newline at end of file diff --git a/apps/agents/.env.example b/apps/agents/.env.example index 65d626a2..fcbeb83b 100644 --- a/apps/agents/.env.example +++ b/apps/agents/.env.example @@ -1 +1,2 @@ -OPENAI_API_KEY= \ No newline at end of file +OPENAI_API_KEY= +API_KEY=test \ No newline at end of file diff --git a/apps/agents/README.md b/apps/agents/README.md index 0fdaca23..7e220f82 100644 --- a/apps/agents/README.md +++ b/apps/agents/README.md @@ -74,8 +74,9 @@ Copy `.env.example` to `.env` and add your API keys - To set up the server on a remote machine: `gunicorn -b 0.0.0.0:4040 src.app.main:app` ### 🖥️ Run test client -`python -m tests.app_client --sample_request default_example.json` +`python -m tests.app_client --sample_request default_example.json --api_key test` - `--sample_request`: Path to the sample request file, under `tests/sample_requests` folder +- `--api_key`: API key to use for authentication. This is the same key as the one in the `.env` file. ## 📖 More details diff --git a/apps/agents/poetry.lock b/apps/agents/poetry.lock index bde28512..b960a8b4 100644 --- a/apps/agents/poetry.lock +++ b/apps/agents/poetry.lock @@ -342,6 +342,29 @@ Werkzeug = ">=3.1" async = ["asgiref (>=3.2)"] dotenv = ["python-dotenv"] +[[package]] +name = "gunicorn" +version = "23.0.0" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, + {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] +tornado = ["tornado (>=0.2)"] + [[package]] name = "h11" version = "0.14.0" @@ -930,6 +953,19 @@ files = [ [package.dependencies] et-xmlfile = "*" +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + [[package]] name = "pandas" version = "2.2.3" @@ -1619,4 +1655,4 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "789b01787d1fa737ae3ad38f642c5c1bcbb142fd38bbbd7a3906a9300b232eb3" +content-hash = "ef220af6b184e760ccc9b45e26ec9f58a54cb9327c562a612798433a7f9c08e4" diff --git a/apps/agents/pyproject.toml b/apps/agents/pyproject.toml index f5380f5e..872c69a5 100644 --- a/apps/agents/pyproject.toml +++ b/apps/agents/pyproject.toml @@ -63,4 +63,5 @@ tzdata = "^2024.2" urllib3 = "^2.2.3" websockets = "^13.1" Werkzeug = "^3.0.5" -wheel = "^0.44.0" \ No newline at end of file +wheel = "^0.44.0" +gunicorn = "^23.0.0" diff --git a/apps/agents/requirements.txt b/apps/agents/requirements.txt index 9135f166..54a25f05 100644 --- a/apps/agents/requirements.txt +++ b/apps/agents/requirements.txt @@ -1,51 +1,82 @@ annotated-types==0.7.0 -anyio==4.6.2 +anyio==4.8.0 beautifulsoup4==4.12.3 -blinker==1.8.2 -certifi==2024.8.30 -charset-normalizer==3.4.0 -click==8.1.7 +blinker==1.9.0 +build==1.2.2.post1 +CacheControl==0.14.2 +certifi==2024.12.14 +cffi==1.17.1 +charset-normalizer==3.4.1 +cleo==2.1.0 +click==8.1.8 +crashtest==0.4.1 +distlib==0.3.9 distro==1.9.0 dnspython==2.7.0 +dulwich==0.22.7 et_xmlfile==2.0.0 -eval_type_backport==0.2.0 -firecrawl==1.4.0 -Flask==3.0.3 +eval_type_backport==0.2.2 +fastjsonschema==2.21.1 +filelock==3.17.0 +firecrawl==1.9.0 +Flask==3.1.0 +gunicorn==23.0.0 h11==0.14.0 -httpcore==1.0.6 +httpcore==1.0.7 httpx==0.27.2 idna==3.10 +installer==0.7.0 itsdangerous==2.2.0 -Jinja2==3.1.4 +jaraco.classes==3.4.0 +jaraco.context==6.0.1 +jaraco.functools==4.1.0 +Jinja2==3.1.5 jiter==0.6.1 jsonpath-python==1.0.6 +keyring==25.6.0 lxml==5.3.0 markdownify==0.13.1 MarkupSafe==3.0.2 +more-itertools==10.6.0 +msgpack==1.1.0 mypy-extensions==1.0.0 nest-asyncio==1.6.0 -numpy==2.1.2 -openai==1.52.2 +numpy==2.2.1 +openai==1.59.7 openpyxl==3.1.5 +packaging==24.2 pandas==2.2.3 -pydantic==2.9.2 -pydantic_core==2.23.4 +pkginfo==1.12.0 +platformdirs==4.3.6 +poetry==2.0.1 +poetry-core==2.0.1 +pycparser==2.22 +pydantic==2.10.5 +pydantic_core==2.27.2 pymongo==4.10.1 -python-dateutil==2.8.2 +pyproject_hooks==1.2.0 +python-dateutil==2.9.0.post0 python-docx==1.1.2 python-dotenv==1.0.1 pytz==2024.2 +RapidFuzz==3.11.0 requests==2.32.3 -setuptools==75.1.0 -six==1.16.0 +requests-toolbelt==1.0.0 +setuptools==75.8.0 +shellingham==1.5.4 +six==1.17.0 sniffio==1.3.1 soupsieve==2.6 tabulate==0.9.0 -tqdm==4.66.5 +tomlkit==0.13.2 +tqdm==4.67.1 +trove-classifiers==2025.1.15.22 typing-inspect==0.9.0 typing_extensions==4.12.2 tzdata==2024.2 -urllib3==2.2.3 +urllib3==2.3.0 +virtualenv==20.29.1 websockets==13.1 -Werkzeug==3.0.5 -wheel==0.44.0 \ No newline at end of file +Werkzeug==3.1.3 +wheel==0.44.0 +xattr==1.1.4 diff --git a/apps/agents/src/app/main.py b/apps/agents/src/app/main.py index 4858127a..d1cde1ad 100644 --- a/apps/agents/src/app/main.py +++ b/apps/agents/src/app/main.py @@ -1,5 +1,7 @@ from flask import Flask, request, jsonify from datetime import datetime +from functools import wraps +import os from src.graph.core import run_turn from src.graph.tools import RAG_TOOL, CLOSE_CHAT_TOOL @@ -9,11 +11,30 @@ logger = common_logger app = Flask(__name__) +@app.route("/health", methods=["GET"]) +def health(): + return jsonify({"status": "ok"}) + @app.route("/") def home(): return "Hello, World!" +def require_api_key(f): + @wraps(f) + def decorated(*args, **kwargs): + auth_header = request.headers.get('Authorization') + if not auth_header or not auth_header.startswith('Bearer '): + return jsonify({'error': 'Missing or invalid authorization header'}), 401 + + token = auth_header.split('Bearer ')[1] + if token != os.environ.get('API_KEY'): + return jsonify({'error': 'Invalid API key'}), 403 + + return f(*args, **kwargs) + return decorated + @app.route("/chat", methods=["POST"]) +@require_api_key def chat(): print('='*200) logger.info('='*200) diff --git a/apps/agents/tests/app_client.py b/apps/agents/tests/app_client.py index b3fd6f79..0669a97c 100644 --- a/apps/agents/tests/app_client.py +++ b/apps/agents/tests/app_client.py @@ -7,13 +7,16 @@ if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument('--sample_request', type=str, required=True, help='Sample request JSON file name under tests/sample_requests/') + parser.add_argument('--api_key', type=str, required=True, help='API key to use for authentication') + parser.add_argument('--host', type=str, required=False, help='Host to use for the request', default='http://localhost:4040') args = parser.parse_args() request = read_json_from_file(f"./tests/sample_requests/{args.sample_request}").get("lastRequest", {}) print("Sending request...") response = requests.post( - "http://localhost:4040/chat", - json=request + f"{args.host}/chat", + json=request, + headers={'Authorization': f'Bearer {args.api_key}'} ).json() print("Output: ") print(response) \ No newline at end of file diff --git a/apps/copilot/README.md b/apps/copilot/README.md index bd807506..05175953 100644 --- a/apps/copilot/README.md +++ b/apps/copilot/README.md @@ -24,6 +24,7 @@ pip install -r requirements.txt 4. Set up your OpenAI API key: ```bash export OPENAI_API_KEY='your-api-key-here' # On Windows, use: set OPENAI_API_KEY=your-api-key-here +export API_KEY='test-api-key' # set a shared API key for the application ``` ## Running the Application @@ -33,24 +34,27 @@ export OPENAI_API_KEY='your-api-key-here' # On Windows, use: set OPENAI_API_KEY python app.py ``` -The server will start on `http://localhost:5000` +The server will start on `http://localhost:3002` ## API Usage The application exposes a single endpoint at `/chat` that accepts POST requests. ### Example Request: -```json -{ - "messages": [ - { - "role": "user", - "content": "Your message here" - } - ], - "workflow_schema": "Your workflow schema here", - "current_workflow_config": "Your current workflow configuration here" -} +```bash +curl -X POST http://localhost:3002/chat \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer test-api-key" \ + -d '{ + "messages": [ + { + "role": "user", + "content": "Your message here" + } + ], + "workflow_schema": "Your workflow schema here", + "current_workflow_config": "Your current workflow configuration here" + }' ``` ### Example Response: diff --git a/apps/copilot/app.py b/apps/copilot/app.py index 0c0c2454..180de34c 100644 --- a/apps/copilot/app.py +++ b/apps/copilot/app.py @@ -3,6 +3,8 @@ from pydantic import BaseModel, ValidationError from typing import List from copilot import UserMessage, AssistantMessage, get_response from lib import AgentContext, PromptContext, ToolContext, ChatContext +import os +from functools import wraps class ApiRequest(BaseModel): messages: List[UserMessage | AssistantMessage] @@ -24,7 +26,26 @@ def validate_request(request_data: ApiRequest) -> None: if not isinstance(request_data.messages[-1], UserMessage): raise ValueError('Last message must be a user message') +def require_api_key(f): + @wraps(f) + def decorated(*args, **kwargs): + auth_header = request.headers.get('Authorization') + if not auth_header or not auth_header.startswith('Bearer '): + return jsonify({'error': 'Missing or invalid authorization header'}), 401 + + token = auth_header.split('Bearer ')[1] + if token != os.environ.get('API_KEY'): + return jsonify({'error': 'Invalid API key'}), 403 + + return f(*args, **kwargs) + return decorated + +@app.route('/health', methods=['GET']) +def health(): + return jsonify({'status': 'ok'}) + @app.route('/chat', methods=['POST']) +@require_api_key def chat(): try: # Log incoming request @@ -74,4 +95,4 @@ def chat(): if __name__ == '__main__': print("Starting Flask server...") - app.run(port=5000, debug=True) \ No newline at end of file + app.run(port=3002, host='0.0.0.0', debug=True) \ No newline at end of file diff --git a/apps/copilot/requirements.txt b/apps/copilot/requirements.txt index 72d3c28f..13444334 100644 --- a/apps/copilot/requirements.txt +++ b/apps/copilot/requirements.txt @@ -5,6 +5,7 @@ certifi==2024.8.30 click==8.1.7 distro==1.9.0 Flask==3.1.0 +gunicorn==23.0.0 h11==0.14.0 httpcore==1.0.7 httpx==0.28.0 @@ -14,6 +15,8 @@ Jinja2==3.1.4 jiter==0.8.0 MarkupSafe==3.0.2 openai==1.57.0 +packaging==24.2 +pydantic==2.10.3 pydantic_core==2.27.1 sniffio==1.3.1 tqdm==4.67.1 diff --git a/apps/rowboat/app/actions.ts b/apps/rowboat/app/actions.ts index 44147cf1..ce6be36c 100644 --- a/apps/rowboat/app/actions.ts +++ b/apps/rowboat/app/actions.ts @@ -504,6 +504,7 @@ export async function getCopilotResponse( body: JSON.stringify(request), headers: { 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.COPILOT_API_KEY}`, }, }); if (!response.ok) { diff --git a/apps/rowboat/app/lib/utils.ts b/apps/rowboat/app/lib/utils.ts index a302a273..a146aad1 100644 --- a/apps/rowboat/app/lib/utils.ts +++ b/apps/rowboat/app/lib/utils.ts @@ -162,11 +162,12 @@ export async function getAgenticApiResponse( }> { // call agentic api console.log(`agentic request`, JSON.stringify(request, null, 2)); - const response = await fetch(process.env.AGENTIC_API_URL + '/chat', { + const response = await fetch(process.env.AGENTS_API_URL + '/chat', { method: 'POST', body: JSON.stringify(request), headers: { 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.AGENTS_API_KEY}`, }, }); if (!response.ok) { diff --git a/docker-compose.yml b/docker-compose.yml index 9d008800..76403396 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,8 +14,10 @@ services: - OXYLABS_USERNAME=${OXYLABS_USERNAME} - OXYLABS_PASSWORD=${OXYLABS_PASSWORD} - CHAT_WIDGET_SESSION_JWT_SECRET=${CHAT_WIDGET_SESSION_JWT_SECRET} - - AGENTIC_API_URL=http://agents:3001 + - AGENTS_API_URL=http://agents:3001 + - AGENTS_API_KEY=${AGENTS_API_KEY} - COPILOT_API_URL=http://copilot:3002 + - COPILOT_API_KEY=${COPILOT_API_KEY} - AUTH0_SECRET=${AUTH0_SECRET} - AUTH0_BASE_URL=${AUTH0_BASE_URL} - AUTH0_ISSUER_BASE_URL=${AUTH0_ISSUER_BASE_URL} @@ -31,6 +33,7 @@ services: - "3001:3001" environment: - OPENAI_API_KEY=${OPENAI_API_KEY} + - API_KEY=${AGENTS_API_KEY} restart: unless-stopped copilot: @@ -41,6 +44,7 @@ services: - "3002:3002" environment: - OPENAI_API_KEY=${OPENAI_API_KEY} + - API_KEY=${COPILOT_API_KEY} restart: unless-stopped docs: