Skip to content

Commit f4e9e50

Browse files
authored
shimmy adoption (#37)
1 parent 4caf532 commit f4e9e50

12 files changed

Lines changed: 162 additions & 154 deletions

AGENTS.md

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ This file provides guidance to AI agents when working with code in this reposito
44

55
## Project Overview
66

7-
This is a boilerplate for creating AI educational chatbots that integrate 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`).
7+
This is a boilerplate for creating AI educational chatbots that integrate 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`).
88

99
## Commands
1010

1111
**Testing:**
1212
```bash
13-
pytest # Run all unit tests
13+
PYTHONPATH=. pytest # Run all unit tests (CI sets PYTHONPATH=. too)
1414
python tests/manual_agent_run.py # Test agent locally with example inputs
1515
python tests/manual_agent_requests.py # Test running Docker container
1616
```
@@ -23,39 +23,45 @@ docker run --env-file .env -p 8080:8080 llm_chat
2323

2424
**Manual API test (while Docker is running):**
2525
```bash
26-
curl -X POST http://localhost:8080/2015-03-31/functions/function/invocations \
26+
curl -X POST http://localhost:8080/chat \
2727
-H 'Content-Type: application/json' \
28-
-d '{"body":"{\"messages\": [{\"role\": \"USER\", \"content\": \"hi\"}]}"}'
28+
-H 'X-Api-Version: 0.1.0' \
29+
-d '{"messages": [{"role": "USER", "content": "hi"}]}'
30+
31+
curl http://localhost:8080/chat/health -H 'X-Api-Version: 0.1.0'
2932
```
3033

3134
**Run a single test:**
3235
```bash
3336
pytest tests/test_module.py # Run specific test file
34-
pytest tests/test_index.py::test_function_name # Run specific test
37+
pytest tests/test_module.py::TestChatModuleFunction::test_response_format # Run specific test
3538
```
3639

3740
## Architecture
3841

3942
### Request Flow
4043

4144
```
42-
Lambda event → index.py (handler)
43-
→ validates via lf_toolkit ChatRequest schema
44-
→ src/module.py (chat_module)
45-
→ extracts muEd API context (messages, conversationId, question context, user type)
46-
→ parses educational context to prompt text via src/agent/context.py
47-
→ src/agent/agent.py (BaseAgent / LangGraph)
48-
→ routes to call_llm or summarize_conversation node
49-
→ calls LLM provider (OpenAI / Google / Azure / Ollama)
50-
→ returns ChatResponse (output, summary, conversationalStyle, processingTime)
45+
shimmy (shim, container entrypoint)
46+
→ spawns index.py as a persistent worker subprocess (lf_toolkit RPC server)
47+
→ forwards POST /chat / GET /chat/health as JSON-RPC "chat" / "chat/health" calls
48+
→ index.py registers src/module.py's chat_module / chat_health_module as handlers
49+
→ lf_toolkit validates the request body against the muEd ChatRequest schema
50+
→ src/module.py (chat_module)
51+
→ extracts muEd API context (messages, conversationId, question context, user type)
52+
→ parses educational context to prompt text via src/agent/context.py
53+
→ src/agent/agent.py (BaseAgent / LangGraph)
54+
→ routes to call_llm or summarize_conversation node
55+
→ calls LLM provider (OpenAI / Google / Azure / Ollama)
56+
→ returns ChatResponse (output, summary, conversationalStyle, processingTime)
5157
```
5258

5359
### Key Files
5460

5561
| File | Role |
5662
|------|------|
57-
| `index.py` | AWS Lambda entry point; parses event body, validates schema |
58-
| `src/module.py` | Transforms muEd API request → invokes agent → builds ChatResponse |
63+
| `index.py` | Worker entrypoint; registers `chat_module`/`chat_health_module` with `lf_toolkit`'s RPC server (`create_server()` + `run()`) |
64+
| `src/module.py` | Transforms muEd API request → invokes agent → builds ChatResponse; also exposes `chat_health_module()` |
5965
| `src/agent/agent.py` | LangGraph stateful graph; manages message history and summarization |
6066
| `src/agent/prompts.py` | System prompts for tutor behavior, summarization, style detection |
6167
| `src/agent/llm_factory.py` | Factory classes for each LLM provider (OpenAI, Google, Azure, Ollama) |

CLAUDE.md

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
This is a boilerplate for creating AI educational chatbots that integrate 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`).
7+
This is a boilerplate for creating AI educational chatbots that integrate 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`).
88

99
## Commands
1010

1111
**Testing:**
1212
```bash
13-
pytest # Run all unit tests
13+
PYTHONPATH=. pytest # Run all unit tests (CI sets PYTHONPATH=. too)
1414
python tests/manual_agent_run.py # Test agent locally with example inputs
1515
python tests/manual_agent_requests.py # Test running Docker container
1616
```
@@ -23,39 +23,45 @@ docker run --env-file .env -p 8080:8080 llm_chat
2323

2424
**Manual API test (while Docker is running):**
2525
```bash
26-
curl -X POST http://localhost:8080/2015-03-31/functions/function/invocations \
26+
curl -X POST http://localhost:8080/chat \
2727
-H 'Content-Type: application/json' \
28-
-d '{"body":"{\"messages\": [{\"role\": \"USER\", \"content\": \"hi\"}]}"}'
28+
-H 'X-Api-Version: 0.1.0' \
29+
-d '{"messages": [{"role": "USER", "content": "hi"}]}'
30+
31+
curl http://localhost:8080/chat/health -H 'X-Api-Version: 0.1.0'
2932
```
3033

3134
**Run a single test:**
3235
```bash
3336
pytest tests/test_module.py # Run specific test file
34-
pytest tests/test_index.py::test_function_name # Run specific test
37+
pytest tests/test_module.py::TestChatModuleFunction::test_response_format # Run specific test
3538
```
3639

3740
## Architecture
3841

3942
### Request Flow
4043

4144
```
42-
Lambda event → index.py (handler)
43-
→ validates via lf_toolkit ChatRequest schema
44-
→ src/module.py (chat_module)
45-
→ extracts muEd API context (messages, conversationId, question context, user type)
46-
→ parses educational context to prompt text via src/agent/context.py
47-
→ src/agent/agent.py (BaseAgent / LangGraph)
48-
→ routes to call_llm or summarize_conversation node
49-
→ calls LLM provider (OpenAI / Google / Azure / Ollama)
50-
→ returns ChatResponse (output, summary, conversationalStyle, processingTime)
45+
shimmy (shim, container entrypoint)
46+
→ spawns index.py as a persistent worker subprocess (lf_toolkit RPC server)
47+
→ forwards POST /chat / GET /chat/health as JSON-RPC "chat" / "chat/health" calls
48+
→ index.py registers src/module.py's chat_module / chat_health_module as handlers
49+
→ lf_toolkit validates the request body against the muEd ChatRequest schema
50+
→ src/module.py (chat_module)
51+
→ extracts muEd API context (messages, conversationId, question context, user type)
52+
→ parses educational context to prompt text via src/agent/context.py
53+
→ src/agent/agent.py (BaseAgent / LangGraph)
54+
→ routes to call_llm or summarize_conversation node
55+
→ calls LLM provider (OpenAI / Google / Azure / Ollama)
56+
→ returns ChatResponse (output, summary, conversationalStyle, processingTime)
5157
```
5258

5359
### Key Files
5460

5561
| File | Role |
5662
|------|------|
57-
| `index.py` | AWS Lambda entry point; parses event body, validates schema |
58-
| `src/module.py` | Transforms muEd API request → invokes agent → builds ChatResponse |
63+
| `index.py` | Worker entrypoint; registers `chat_module`/`chat_health_module` with `lf_toolkit`'s RPC server (`create_server()` + `run()`) |
64+
| `src/module.py` | Transforms muEd API request → invokes agent → builds ChatResponse; also exposes `chat_health_module()` |
5965
| `src/agent/agent.py` | LangGraph stateful graph; manages message history and summarization |
6066
| `src/agent/prompts.py` | System prompts for tutor behavior, summarization, style detection |
6167
| `src/agent/llm_factory.py` | Factory classes for each LLM provider (OpenAI, Google, Azure, Ollama) |

Dockerfile

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
1-
ARG PYTHON_VERSION=3.13
1+
ARG BASE_VERSION=python:3.12
22

3-
FROM public.ecr.aws/lambda/python:${PYTHON_VERSION}
3+
# evaluation-function-base's python image bundles the shimmy binary,
4+
# the Lambda RIE, and the entrypoint.sh that picks between them.
5+
FROM ghcr.io/lambda-feedback/evaluation-function-base/${BASE_VERSION}
46

5-
# Set working directory
6-
WORKDIR ${LAMBDA_TASK_ROOT}
7+
RUN apt-get update && apt-get install -y \
8+
build-essential \
9+
&& rm -rf /var/lib/apt/lists/*
710

8-
RUN pip install --upgrade pip
9-
RUN dnf install -y git \
10-
&& dnf install -y \
11-
gcc \
12-
gcc-c++ \
13-
make \
14-
python3-devel \
15-
&& dnf clean all
11+
RUN pip install --upgrade pip
1612

1713
COPY requirements.txt .
1814
RUN pip install -r requirements.txt
@@ -27,5 +23,15 @@ COPY index.py .
2723

2824
COPY tests ./tests
2925

30-
# Set the Lambda function handler
31-
CMD ["index.handler"]
26+
# Command shimmy uses to start the chat function worker
27+
ENV FUNCTION_COMMAND="python"
28+
29+
# Args to start the chat function worker with
30+
ENV FUNCTION_ARGS="index.py"
31+
32+
# The transport to use for the RPC server
33+
ENV FUNCTION_RPC_TRANSPORT="ipc"
34+
35+
ENV FUNCTION_WORKER_SEND_TIMEOUT="170s"
36+
37+
ENV LOG_LEVEL="debug"

README.md

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ The agent uses **two separate LLM instances** — `self.llm` for chat responses
113113
├── manual_agent_run.py # allows testing of any LLM agent on a couple of example inputs
114114
├── utils.py # shared test helpers
115115
├── test_example_inputs.py # pytests for the example input files
116-
├── test_index.py # pytests
117116
└── test_module.py # pytests
118117
```
119118

@@ -124,18 +123,18 @@ To test your function, you can run the unit tests, call the code directly throug
124123

125124
### Run Unit Tests
126125

127-
You can run the unit tests using `pytest`.
126+
You can run the unit tests using `pytest`. Run it from the repository root with `PYTHONPATH=.` set (as CI does) so the `tests` and `src` packages resolve correctly:
128127

129128
```bash
130-
pytest
129+
PYTHONPATH=. pytest
131130
```
132131

133132
### Run the Chat Script
134133

135-
You can run the Python function itself. Make sure to have a main function in either `src/module.py` or `index.py`.
134+
You can run the Python function itself directly — `index.py` wires `chat_module`/`chat_health_module` into `lf_toolkit`'s RPC server, the same way shimmy invokes it inside the container. This requires the `EVAL_IO`/`EVAL_RPC_TRANSPORT` environment variables shimmy would normally set (see `lf_toolkit`'s docs), so prefer the Docker or `manual_agent_run.py` routes below for everyday testing.
136135

137136
```bash
138-
python src/module.py
137+
python index.py
139138
```
140139

141140
You can also use the `manual_agent_run.py` script to test the agents with example inputs from Lambda Feedback questions and synthetic conversations.
@@ -167,33 +166,41 @@ docker run -e OPENAI_API_KEY={your key} -e OPENAI_MODEL={your LLM model name} -p
167166
docker run --env-file .env -it --name my-lambda-container -p 8080:8080 llm_chat
168167
```
169168

170-
This will start the chat function and expose it on port `8080` and it will be open to be curl:
169+
This starts shimmy (the [Lambda Feedback shim](https://github.com/lambda-feedback/shimmy)) as the container's entrypoint, which spawns this function as a worker subprocess and exposes it on port `8080` as the muEd chat API:
171170

172171
```bash
173-
curl --location 'http://localhost:8080/2015-03-31/functions/function/invocations' \
172+
curl --location 'http://localhost:8080/chat' \
174173
--header 'Content-Type: application/json' \
175-
--data '{"body":"{\"messages\": [{\"role\": \"USER\", \"content\": \"hi\"}]}"}'
174+
--header 'X-Api-Version: 0.1.0' \
175+
--data '{"messages": [{"role": "USER", "content": "hi"}]}'
176+
```
177+
178+
Health check:
179+
180+
```bash
181+
curl --location 'http://localhost:8080/chat/health' \
182+
--header 'X-Api-Version: 0.1.0'
176183
```
177184

178185
#### Call Docker Container
179186
##### A. Call Docker with Python Requests
180187

181-
In the `tests/` folder you can find the `manual_agent_requests.py` script that calls the POST URL of the running docker container. It reads any kind of input files with the expected schema. You can use this to test your curl calls of the chatbot.
188+
In the `tests/` folder you can find the `manual_agent_requests.py` script that calls the `/chat` and `/chat/health` routes of the running docker container. It reads any kind of input files with the expected schema. You can use this to test your curl calls of the chatbot.
182189

183190
##### B. Call Docker Container through API request
184191

185192
POST URL:
186193

187194
```bash
188-
http://localhost:8080/2015-03-31/functions/function/invocations
195+
http://localhost:8080/chat
189196
```
190197

191-
Per the [muEd `ChatRequest` schema](https://mued.org/), only `messages` is required; `conversationId`, `user`, `context`, and `configuration` are all optional.
198+
Per the [muEd `ChatRequest` schema](https://mued.org/), only `messages` is required; `conversationId`, `user`, `context`, and `configuration` are all optional. Requests may include an `X-Api-Version: 0.1.0` header.
192199

193-
**Minimal request — only required components** (stringified within `body` for the AWS Lambda Runtime Interface Emulator):
200+
**Minimal request — only required components:**
194201

195202
```JSON
196-
{"body":"{\"messages\": [{\"role\": \"USER\", \"content\": \"hi\"}]}"}
203+
{"messages": [{"role": "USER", "content": "hi"}]}
197204
```
198205

199206
**Full request as Lambda Feedback sends it** — all optional fields populated:

docs/dev.md

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ To test your function, you can run the unit tests, call the code directly throug
1616

1717
### Run Unit Tests
1818

19-
You can run the unit tests using `pytest`.
19+
You can run the unit tests using `pytest`. Run it from the repository root with `PYTHONPATH=.` set (as CI does) so the `tests` and `src` packages resolve correctly:
2020

2121
```bash
22-
pytest
22+
PYTHONPATH=. pytest
2323
```
2424

2525
### Run the Chat Script
@@ -53,31 +53,39 @@ docker run -e OPENAI_API_KEY={your key} -e OPENAI_MODEL={your LLM chosen model n
5353
docker run --env-file .env -it --name my-lambda-container -p 8080:8080 llm_chat
5454
```
5555

56-
This will start the chat function and expose it on port `8080` and it will be open to be curl:
56+
This starts shimmy (the [Lambda Feedback shim](https://github.com/lambda-feedback/shimmy)) as the container's entrypoint, which spawns this function as a worker subprocess and exposes it on port `8080` as the muEd chat API:
5757

5858
```bash
59-
curl --location 'http://localhost:8080/2015-03-31/functions/function/invocations' \
59+
curl --location 'http://localhost:8080/chat' \
6060
--header 'Content-Type: application/json' \
61-
--data '{"body":"{\"conversationId\": \"12345Test\", \"messages\": [{\"role\": \"USER\", \"content\": \"hi\"}], \"user\": {\"type\": \"LEARNER\"}}"}'
61+
--header 'X-Api-Version: 0.1.0' \
62+
--data '{"conversationId": "12345Test", "messages": [{"role": "USER", "content": "hi"}], "user": {"type": "LEARNER"}}'
63+
```
64+
65+
Health check:
66+
67+
```bash
68+
curl --location 'http://localhost:8080/chat/health' \
69+
--header 'X-Api-Version: 0.1.0'
6270
```
6371

6472
#### Call Docker Container
6573
##### A. Call Docker with Python Requests
6674

67-
In the `tests/` folder you can find the `manual_agent_requests.py` script that calls the POST URL of the running docker container. It reads any kind of input files with the expected schema. You can use this to test your curl calls of the chatbot.
75+
In the `tests/` folder you can find the `manual_agent_requests.py` script that calls the `/chat` and `/chat/health` routes of the running docker container. It reads any kind of input files with the expected schema. You can use this to test your curl calls of the chatbot.
6876

6977
##### B. Call Docker Container through API request
7078

7179
POST URL:
7280

7381
```bash
74-
http://localhost:8080/2015-03-31/functions/function/invocations
82+
http://localhost:8080/chat
7583
```
7684

77-
Body (stringified within body for API request):
85+
Body (requests may include an `X-Api-Version: 0.1.0` header):
7886

7987
```JSON
80-
{"body":"{\"conversationId\": \"12345Test\", \"messages\": [{\"role\": \"USER\", \"content\": \"hi\"}], \"user\": {\"type\": \"LEARNER\"}}"}
88+
{"conversationId": "12345Test", "messages": [{"role": "USER", "content": "hi"}], "user": {"type": "LEARNER"}}
8189
```
8290

8391
Body with optional fields:

index.py

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,14 @@
1-
import json
2-
from pydantic import ValidationError
1+
from lf_toolkit import create_server, run
32

4-
from lf_toolkit.chat import ChatRequest
5-
from src.module import chat_module
3+
from src.module import chat_health_module, chat_module
64

75

8-
def handler(event, context):
9-
"""
10-
Lambda handler function
11-
"""
12-
if "body" in event:
13-
try:
14-
event = json.loads(event["body"])
15-
except json.JSONDecodeError:
16-
return {
17-
"statusCode": 400,
18-
"body": "Invalid JSON format in the body. Please check the input.",
19-
}
6+
def main():
7+
server = create_server()
8+
server.chat(chat_module)
9+
server.chat_health(chat_health_module)
10+
run(server)
2011

21-
try:
22-
request = ChatRequest.model_validate(event)
23-
except ValidationError as e:
24-
return {"statusCode": 400, "body": e.json()}
2512

26-
try:
27-
result = chat_module(request)
28-
except Exception as e:
29-
return {
30-
"statusCode": 500,
31-
"body": f"An error occurred within the chat_module(): {str(e)}",
32-
}
33-
34-
response = {"statusCode": 200, "body": result.model_dump_json()}
35-
return response
13+
if __name__ == "__main__":
14+
main()

0 commit comments

Comments
 (0)