Skip to content

Commit fb7b1e0

Browse files
authored
Merge pull request #9 from lambda-feedback/shimmy
Shimmy
2 parents 0270b04 + ab9c3b0 commit fb7b1e0

12 files changed

Lines changed: 141 additions & 140 deletions

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 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`).
7+
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`).
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
@@ -119,7 +119,6 @@ The agent uses **two separate LLM instances** — `self.llm` for chat responses
119119
├── manual_agent_run.py # allows testing of any LLM agent on a couple of example inputs
120120
├── utils.py # shared test helpers
121121
├── test_example_inputs.py # pytests for the example input files
122-
├── test_index.py # pytests
123122
└── test_module.py # pytests
124123
```
125124

@@ -130,18 +129,18 @@ To test your function, you can run the unit tests, call the code directly throug
130129

131130
### Run Unit Tests
132131

133-
You can run the unit tests using `pytest`.
132+
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:
134133

135134
```bash
136-
pytest
135+
PYTHONPATH=. pytest
137136
```
138137

139138
### Run the Chat Script
140139

141-
You can run the Python function itself. Make sure to have a main function in either `src/module.py` or `index.py`.
140+
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.
142141

143142
```bash
144-
python src/module.py
143+
python index.py
145144
```
146145

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

176-
This will start the chat function and expose it on port `8080` and it will be open to be curl:
175+
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:
177176

178177
```bash
179-
curl --location 'http://localhost:8080/2015-03-31/functions/function/invocations' \
178+
curl --location 'http://localhost:8080/chat' \
180179
--header 'Content-Type: application/json' \
181-
--data '{"body":"{\"messages\": [{\"role\": \"USER\", \"content\": \"hi\"}]}"}'
180+
--header 'X-Api-Version: 0.1.0' \
181+
--data '{"messages": [{"role": "USER", "content": "hi"}]}'
182+
```
183+
184+
Health check:
185+
186+
```bash
187+
curl --location 'http://localhost:8080/chat/health' \
188+
--header 'X-Api-Version: 0.1.0'
182189
```
183190

184191
#### Call Docker Container
185192
##### A. Call Docker with Python Requests
186193

187-
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.
194+
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.
188195

189196
##### B. Call Docker Container through API request
190197

191198
POST URL:
192199

193200
```bash
194-
http://localhost:8080/2015-03-31/functions/function/invocations
201+
http://localhost:8080/chat
195202
```
196203

197-
Per the [muEd `ChatRequest` schema](https://mued.org/), only `messages` is required; `conversationId`, `user`, `context`, and `configuration` are all optional.
204+
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.
198205

199-
**Minimal request — only required components** (stringified within `body` for the AWS Lambda Runtime Interface Emulator):
206+
**Minimal request — only required components:**
200207

201208
```JSON
202-
{"body":"{\"messages\": [{\"role\": \"USER\", \"content\": \"hi\"}]}"}
209+
{"messages": [{"role": "USER", "content": "hi"}]}
203210
```
204211

205212
**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
@@ -28,10 +28,10 @@ To test your function, you can run the unit tests, call the code directly throug
2828

2929
### Run Unit Tests
3030

31-
You can run the unit tests using `pytest`.
31+
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:
3232

3333
```bash
34-
pytest
34+
PYTHONPATH=. pytest
3535
```
3636

3737
### Run the Chat Script
@@ -65,31 +65,39 @@ docker run -e OPENAI_API_KEY={your key} -e OPENAI_MODEL={your LLM chosen model n
6565
docker run --env-file .env -it --name my-lambda-container -p 8080:8080 llm_chat
6666
```
6767

68-
This will start the chat function and expose it on port `8080` and it will be open to be curl:
68+
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:
6969

7070
```bash
71-
curl --location 'http://localhost:8080/2015-03-31/functions/function/invocations' \
71+
curl --location 'http://localhost:8080/chat' \
7272
--header 'Content-Type: application/json' \
73-
--data '{"body":"{\"conversationId\": \"12345Test\", \"messages\": [{\"role\": \"USER\", \"content\": \"hi\"}], \"user\": {\"type\": \"LEARNER\"}}"}'
73+
--header 'X-Api-Version: 0.1.0' \
74+
--data '{"conversationId": "12345Test", "messages": [{"role": "USER", "content": "hi"}], "user": {"type": "LEARNER"}}'
75+
```
76+
77+
Health check:
78+
79+
```bash
80+
curl --location 'http://localhost:8080/chat/health' \
81+
--header 'X-Api-Version: 0.1.0'
7482
```
7583

7684
#### Call Docker Container
7785
##### A. Call Docker with Python Requests
7886

79-
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.
87+
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.
8088

8189
##### B. Call Docker Container through API request
8290

8391
POST URL:
8492

8593
```bash
86-
http://localhost:8080/2015-03-31/functions/function/invocations
94+
http://localhost:8080/chat
8795
```
8896

89-
Input body (stringified within body for API request):
97+
Input body (requests must include an `X-Api-Version: 0.1.0` header):
9098

9199
```JSON
92-
{"body":"{\"conversationId\": \"12345Test\", \"messages\": [{\"role\": \"USER\", \"content\": \"hi\"}], \"user\": {\"type\": \"LEARNER\"}}"}
100+
{"conversationId": "12345Test", "messages": [{"role": "USER", "content": "hi"}], "user": {"type": "LEARNER"}}
93101
```
94102

95103
Body with optional fields:

index.py

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +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-
print("Received event:", json.dumps(event))
6+
def main():
7+
server = create_server()
8+
server.chat(chat_module)
9+
server.chat_health(chat_health_module)
10+
run(server)
1311

14-
if "body" in event:
15-
try:
16-
event = json.loads(event["body"])
17-
except json.JSONDecodeError:
18-
return {
19-
"statusCode": 400,
20-
"body": "Invalid JSON format in the body. Please check the input.",
21-
}
2212

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

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ langdetect
1010
langgraph
1111
langsmith
1212

13-
lf_toolkit[ipc] @ git+https://github.com/lambda-feedback/toolkit-python.git@main
13+
lf_toolkit[ipc] @ git+https://github.com/lambda-feedback/toolkit-python.git@fix/ipc
1414
pytest
1515
flake8

src/module.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import time
22
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
33

4-
from lf_toolkit.chat import ChatRequest, ChatResponse, Message
5-
from lf_toolkit.shared.mued_api_v0_1_0 import Role
4+
from lf_toolkit.chat import ChatCapabilities, ChatHealthResponse, ChatRequest, ChatResponse, Message
5+
from lf_toolkit.shared.mued_api_v0_1_0 import DataPolicySupport, HealthStatus, Role
66

77
from src.agent.context import parse_json_to_prompt
88
from src.agent.agent import invoke_base_agent
@@ -61,6 +61,22 @@ def chat_module(request: ChatRequest) -> ChatResponse:
6161
)
6262

6363

64+
def chat_health_module() -> ChatHealthResponse:
65+
"""
66+
Health-check entry point — reports whether this chat function is up and
67+
what it supports, for the shim's GET /chat/health.
68+
"""
69+
return ChatHealthResponse(
70+
status=HealthStatus.OK,
71+
capabilities=ChatCapabilities(
72+
supportsChat=True,
73+
supportsUserPreferences=True,
74+
supportsStreaming=False,
75+
supportsDataPolicy=DataPolicySupport.NOT_SUPPORTED,
76+
),
77+
)
78+
79+
6480
def _to_langchain_messages(messages):
6581
result = []
6682
for m in messages:

tests/manual_agent_requests.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,34 @@
22
import json
33

44
"""
5-
Script that sends a request to the local endpoint of the docker container to test the chatbot agent.
5+
Script that sends requests straight to shimmy's muEd chat routes on the
6+
locally running docker container (`docker build` and `docker run`) to test
7+
the chatbot agent end-to-end, behind the shim.
68
"""
79

8-
# URL for the local endpoint to docker (`docker build` and `docker run`)
9-
url = "http://localhost:8080/2015-03-31/functions/function/invocations"
10+
base_url = "http://localhost:8080"
11+
12+
headers = {
13+
'Content-Type': 'application/json',
14+
'X-Api-Version': '0.1.0',
15+
}
16+
17+
# Health check
18+
health_response = requests.get(f"{base_url}/chat/health", headers=headers)
19+
print("GET /chat/health ->", health_response.status_code)
20+
print(health_response.text)
1021

1122
# File path for the input text
1223
path = "tests/example_inputs/"
1324
input_file = path + "example_input_1.json"
1425

1526
# Step 1: Read the input file
1627
with open(input_file, "r") as file:
17-
data = file.read()
28+
payload = file.read()
1829

19-
payload = json.dumps({"body": data})
2030
print(payload)
21-
headers = {
22-
'Content-Type': 'application/json'
23-
}
2431

25-
response = requests.request("POST", url, headers=headers, data=payload)
32+
response = requests.post(f"{base_url}/chat", headers=headers, data=payload)
2633

34+
print("POST /chat ->", response.status_code)
2735
print(response.text)

0 commit comments

Comments
 (0)