diff --git a/.github/workflows/production-deploy.yml b/.github/workflows/production-deploy.yml index 9aa6de7..1e97204 100644 --- a/.github/workflows/production-deploy.yml +++ b/.github/workflows/production-deploy.yml @@ -31,7 +31,7 @@ jobs: environment: "production" version-bump: ${{ inputs.version-bump }} branch: ${{ inputs.branch }} - deployed-environment-variables: '[\"OPENAI_API_KEY\",\"OPENAI_MODEL\",\"GOOGLE_AI_API_KEY\",\"GOOGLE_AI_MODEL\"]' + deployed-environment-variables: '[\"OPENAI_API_KEY\",\"OPENAI_MODEL\",\"GOOGLE_AI_API_KEY\",\"GOOGLE_AI_MODEL\",\"OPENROUTER_API_KEY\",\"OPENROUTER_MODEL\",\"OPENROUTER_BASE_URL\"]' secrets: aws-key-id: ${{ secrets.LAMBDA_CONTAINER_PIPELINE_AWS_ID }} aws-secret-key: ${{ secrets.LAMBDA_CONTAINER_PIPELINE_AWS_SECRET }} diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml index 7810a08..8bc4df4 100644 --- a/.github/workflows/staging-deploy.yml +++ b/.github/workflows/staging-deploy.yml @@ -23,6 +23,9 @@ jobs: OPENAI_MODEL: ${{ vars.OPENAI_MODEL }} GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }} GOOGLE_AI_MODEL: ${{ vars.GOOGLE_AI_MODEL }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENROUTER_MODEL: ${{ vars.OPENROUTER_MODEL }} + OPENROUTER_BASE_URL: ${{ vars.OPENROUTER_BASE_URL }} steps: - name: Checkout Code uses: actions/checkout@v4 @@ -67,7 +70,7 @@ jobs: with: template-repository-name: "lambda-feedback/chat-function-boilerplate" environment: "staging" - deployed-environment-variables: '[\"OPENAI_API_KEY\",\"OPENAI_MODEL\",\"GOOGLE_AI_API_KEY\",\"GOOGLE_AI_MODEL\"]' + deployed-environment-variables: '[\"OPENAI_API_KEY\",\"OPENAI_MODEL\",\"GOOGLE_AI_API_KEY\",\"GOOGLE_AI_MODEL\",\"OPENROUTER_API_KEY\",\"OPENROUTER_MODEL\",\"OPENROUTER_BASE_URL\"]' secrets: aws-key-id: ${{ secrets.LAMBDA_CONTAINER_PIPELINE_AWS_ID }} aws-secret-key: ${{ secrets.LAMBDA_CONTAINER_PIPELINE_AWS_SECRET }} diff --git a/.github/workflows/test-lint.yml b/.github/workflows/test-lint.yml index 59115f8..92a5959 100644 --- a/.github/workflows/test-lint.yml +++ b/.github/workflows/test-lint.yml @@ -21,6 +21,9 @@ jobs: OPENAI_MODEL: ${{ vars.OPENAI_MODEL }} GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }} GOOGLE_AI_MODEL: ${{ vars.GOOGLE_AI_MODEL }} + OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} + OPENROUTER_MODEL: ${{ vars.OPENROUTER_MODEL }} + OPENROUTER_BASE_URL: ${{ vars.OPENROUTER_BASE_URL }} steps: - name: Checkout uses: actions/checkout@v4 diff --git a/AGENTS.md b/AGENTS.md index d926dc0..117f13e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,13 +4,13 @@ This file provides guidance to AI agents when working with code in this reposito ## Project Overview -This is a chat function connecting students to an AI educational chatbot that is integrated with the **Lambda-Feedback** educational platform. It deploys as an AWS Lambda function (containerized via Docker) that receives student chat messages with educational context and returns LLM-powered chatbot responses. Incoming requests follow the [muEd API](https://mued.org/) schema (`context`, `user`, `messages`). +This is a chat function connecting students to an AI educational chatbot that is integrated with the **Lambda-Feedback** educational platform. It's containerized via Docker and deployed behind [shimmy](https://github.com/lambda-feedback/shimmy), a shim that spawns this function as a persistent JSON-RPC worker process and exposes it as the muEd `/chat` / `/chat/health` HTTP API (both locally and as an AWS Lambda container). It receives student chat messages with educational context and returns LLM-powered chatbot responses. Incoming requests follow the [muEd API](https://mued.org/) schema (`context`, `user`, `messages`). ## Commands **Testing:** ```bash -pytest # Run all unit tests +PYTHONPATH=. pytest # Run all unit tests (CI sets PYTHONPATH=. too) python tests/manual_agent_run.py # Test agent locally with example inputs python tests/manual_agent_requests.py # Test running Docker container ``` @@ -23,15 +23,18 @@ docker run --env-file .env -p 8080:8080 llm_chat **Manual API test (while Docker is running):** ```bash -curl -X POST http://localhost:8080/2015-03-31/functions/function/invocations \ +curl -X POST http://localhost:8080/chat \ -H 'Content-Type: application/json' \ - -d '{"body":"{\"messages\": [{\"role\": \"USER\", \"content\": \"hi\"}]}"}' + -H 'X-Api-Version: 0.1.0' \ + -d '{"messages": [{"role": "USER", "content": "hi"}]}' + +curl http://localhost:8080/chat/health -H 'X-Api-Version: 0.1.0' ``` **Run a single test:** ```bash pytest tests/test_module.py # Run specific test file -pytest tests/test_index.py::test_function_name # Run specific test +pytest tests/test_module.py::TestChatModuleFunction::test_response_format # Run specific test ``` ## Architecture @@ -39,23 +42,26 @@ pytest tests/test_index.py::test_function_name # Run specific test ### Request Flow ``` -Lambda event → index.py (handler) - → validates via lf_toolkit ChatRequest schema - → src/module.py (chat_module) - → extracts muEd API context (messages, conversationId, question context, user type) - → parses educational context to prompt text via src/agent/context.py - → src/agent/agent.py (BaseAgent / LangGraph) - → routes to call_llm or summarize_conversation node - → calls LLM provider (OpenAI / Google / Azure / Ollama) - → returns ChatResponse (output, summary, conversationalStyle, processingTime) +shimmy (shim, container entrypoint) + → spawns index.py as a persistent worker subprocess (lf_toolkit RPC server) + → forwards POST /chat / GET /chat/health as JSON-RPC "chat" / "chat/health" calls + → index.py registers src/module.py's chat_module / chat_health_module as handlers + → lf_toolkit validates the request body against the muEd ChatRequest schema + → src/module.py (chat_module) + → extracts muEd API context (messages, conversationId, question context, user type) + → parses educational context to prompt text via src/agent/context.py + → src/agent/agent.py (BaseAgent / LangGraph) + → routes to call_llm or summarize_conversation node + → calls LLM provider (OpenAI / Google / Azure / Ollama) + → returns ChatResponse (output, summary, conversationalStyle, processingTime) ``` ### Key Files | File | Role | |------|------| -| `index.py` | AWS Lambda entry point; parses event body, validates schema | -| `src/module.py` | Transforms muEd API request → invokes agent → builds ChatResponse | +| `index.py` | Worker entrypoint; registers `chat_module`/`chat_health_module` with `lf_toolkit`'s RPC server (`create_server()` + `run()`) | +| `src/module.py` | Transforms muEd API request → invokes agent → builds ChatResponse; also exposes `chat_health_module()` | | `src/agent/agent.py` | LangGraph stateful graph; manages message history and summarization | | `src/agent/prompts.py` | System prompts for tutor behavior, summarization, style detection | | `src/agent/llm_factory.py` | Factory classes for each LLM provider (OpenAI, Google, Azure, Ollama) | diff --git a/CLAUDE.md b/CLAUDE.md index 70cf442..400387b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a chat function connecting students to an AI educational chatbot that is integrated with the **Lambda-Feedback** educational platform. It's containerized via Docker and deployed behind [shimmy](https://github.com/lambda-feedback/shimmy), a shim that spawns this function as a persistent JSON-RPC worker process and exposes it as the muEd `/chat` / `/chat/health` HTTP API (both locally and as an AWS Lambda container). Incoming requests follow the [muEd API](https://mued.org/) schema (`context`, `user`, `messages`). +This is a chat function connecting students to an AI educational chatbot that is integrated with the **Lambda-Feedback** educational platform. It's containerized via Docker and deployed behind [shimmy](https://github.com/lambda-feedback/shimmy), a shim that spawns this function as a persistent JSON-RPC worker process and exposes it as the muEd `/chat` / `/chat/health` HTTP API (both locally and as an AWS Lambda container). It receives student chat messages with educational context and returns LLM-powered chatbot responses. Incoming requests follow the [muEd API](https://mued.org/) schema (`context`, `user`, `messages`). ## Commands diff --git a/Dockerfile b/Dockerfile index 150f5a3..72c9d5c 100755 --- a/Dockerfile +++ b/Dockerfile @@ -34,4 +34,4 @@ ENV FUNCTION_RPC_TRANSPORT="ipc" ENV FUNCTION_WORKER_SEND_TIMEOUT="170s" -ENV LOG_LEVEL="debug" \ No newline at end of file +ENV LOG_LEVEL="debug" diff --git a/README.md b/README.md index 4a4f57e..afa74c5 100755 --- a/README.md +++ b/README.md @@ -24,19 +24,15 @@ OPENAI_MODEL GOOGLE_AI_API_KEY GOOGLE_AI_MODEL ``` - -> [!Note] -> If you decide to use another endpoint such as Azure or Ollama or any other, please update the github workflow files to use the right secrets and variables for testing. +> If you use OpenRouter: ```bash -> If you use Azure-OpenAI: -AZURE_OPENAI_API_KEY -AZURE_OPENAI_ENDPOINT -AZURE_OPENAI_API_VERSION -AZURE_OPENAI_CHAT_DEPLOYMENT_NAME -AZURE_OPENAI_EMBEDDING_3072_DEPLOYMENT -AZURE_OPENAI_EMBEDDING_1536_DEPLOYMENT -AZURE_OPENAI_EMBEDDING_3072_MODEL -AZURE_OPENAI_EMBEDDING_1536_MODEL +OPENROUTER_API_KEY +OPENROUTER_MODEL +OPENROUTER_BASE_URL +``` + +> [!NOTE] +> If you decide to use other providers like Azure OpenAI or Ollama, you will need to update the workflow files and the `llm_factory.py` file to include the necessary environment variables for those providers. > For monitoring of the LLM calls (follow instructions on how to set up on langsmith online): LANGCHAIN_TRACING_V2 diff --git a/docs/dev.md b/docs/dev.md index 82c2907..ec85788 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -94,7 +94,7 @@ POST URL: http://localhost:8080/chat ``` -Input body (requests must include an `X-Api-Version: 0.1.0` header): +Body (requests may include an `X-Api-Version: 0.1.0` header): ```JSON {"conversationId": "12345Test", "messages": [{"role": "USER", "content": "hi"}], "user": {"type": "LEARNER"}} diff --git a/src/agent/llm_factory.py b/src/agent/llm_factory.py index 07e887b..efe8e88 100644 --- a/src/agent/llm_factory.py +++ b/src/agent/llm_factory.py @@ -1,4 +1,5 @@ import os +from typing import Optional from langchain_openai import AzureChatOpenAI from langchain_openai import AzureOpenAIEmbeddings @@ -82,3 +83,19 @@ def __init__(self, temperature: int = 0): def get_llm(self): return self._google_llm + +class ChatOpenRouterProvider: + def __init__(self, temperature: int = 0, model: Optional[str] = None): + model_name = model or os.environ['OPENROUTER_MODEL'] + key = os.environ['OPENROUTER_API_KEY'] + base_url = os.environ['OPENROUTER_BASE_URL'] + + self._openrouter_llm = ChatOpenAI( + model=model_name, + temperature=temperature, + api_key=key, + base_url=base_url, + ) + + def get_llm(self): + return self._openrouter_llm \ No newline at end of file diff --git a/src/module.py b/src/module.py index f67240a..5c48299 100755 --- a/src/module.py +++ b/src/module.py @@ -70,7 +70,7 @@ def chat_health_module() -> ChatHealthResponse: status=HealthStatus.OK, capabilities=ChatCapabilities( supportsChat=True, - supportsUserPreferences=True, + supportsUserPreferences=False, supportsStreaming=False, supportsDataPolicy=DataPolicySupport.NOT_SUPPORTED, ), diff --git a/tests/manual_agent_requests.py b/tests/manual_agent_requests.py index 07f2434..03ba27f 100644 --- a/tests/manual_agent_requests.py +++ b/tests/manual_agent_requests.py @@ -1,5 +1,4 @@ import requests -import json """ Script that sends requests straight to shimmy's muEd chat routes on the