feat: add teaching asset prompts

This commit is contained in:
user
2026-04-26 17:54:23 +09:00
parent 4936cdf4c9
commit 156daa9087
22 changed files with 594 additions and 14 deletions

View File

@@ -12,6 +12,7 @@ import (
"tutor/internal/learnermemory"
"tutor/internal/ontology"
"tutor/internal/progression"
"tutor/internal/teachingassets"
"tutor/internal/workflows"
)
@@ -20,7 +21,8 @@ func TestDiagnosticHTTPFlow(t *testing.T) {
service := interview.NewService(interview.NewMemoryStore(), workflows.NewStubRunner(), memory)
progress := progression.NewService(memory)
onto := ontology.NewService(ontology.NewMemoryStore())
handler := NewHandler(config.Config{Environment: "test", ModelKey: "deepseek-v4-flash"}, service, memory, progress, onto)
assets := teachingassets.NewService(teachingassets.NewMemoryStore(), onto, "gpt-image-v2")
handler := NewHandler(config.Config{Environment: "test", ModelKey: "deepseek-v4-flash"}, service, memory, progress, onto, assets)
routes := handler.Routes()
createBody := bytes.NewBufferString(`{

View File

@@ -9,6 +9,7 @@ import (
"tutor/internal/learnermemory"
"tutor/internal/ontology"
"tutor/internal/progression"
"tutor/internal/teachingassets"
)
type Handler struct {
@@ -17,6 +18,7 @@ type Handler struct {
memory *learnermemory.Service
progress *progression.Service
ontology *ontology.Service
assets *teachingassets.Service
}
func NewHandler(
@@ -25,6 +27,7 @@ func NewHandler(
memory *learnermemory.Service,
progress *progression.Service,
ontology *ontology.Service,
assets *teachingassets.Service,
) Handler {
return Handler{
cfg: cfg,
@@ -32,6 +35,7 @@ func NewHandler(
memory: memory,
progress: progress,
ontology: ontology,
assets: assets,
}
}
@@ -46,6 +50,8 @@ func (h Handler) Routes() http.Handler {
mux.HandleFunc("GET /api/v1/learners/{userID}/next-challenge", h.getNextChallenge)
mux.HandleFunc("POST /api/v1/materials", h.ingestMaterial)
mux.HandleFunc("GET /api/v1/ontology", h.getOntology)
mux.HandleFunc("POST /api/v1/teaching-assets/prompts", h.generateTeachingAssetPrompt)
mux.HandleFunc("GET /api/v1/teaching-assets", h.getTeachingAssets)
return mux
}

View File

@@ -11,6 +11,7 @@ import (
"tutor/internal/learnermemory"
"tutor/internal/ontology"
"tutor/internal/progression"
"tutor/internal/teachingassets"
"tutor/internal/workflows"
)
@@ -23,7 +24,8 @@ func TestHealth(t *testing.T) {
service := interview.NewService(interview.NewMemoryStore(), workflows.NewStubRunner(), memory)
progress := progression.NewService(memory)
onto := ontology.NewService(ontology.NewMemoryStore())
handler := NewHandler(cfg, service, memory, progress, onto)
assets := teachingassets.NewService(teachingassets.NewMemoryStore(), onto, cfg.ImageModelKey)
handler := NewHandler(cfg, service, memory, progress, onto, assets)
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
rec := httptest.NewRecorder()

View File

@@ -12,6 +12,7 @@ import (
"tutor/internal/learnermemory"
"tutor/internal/ontology"
"tutor/internal/progression"
"tutor/internal/teachingassets"
"tutor/internal/workflows"
)
@@ -20,7 +21,8 @@ func TestOntologyHTTPFlow(t *testing.T) {
service := interview.NewService(interview.NewMemoryStore(), workflows.NewStubRunner(), memory)
progress := progression.NewService(memory)
onto := ontology.NewService(ontology.NewMemoryStore())
handler := NewHandler(config.Config{Environment: "test"}, service, memory, progress, onto)
assets := teachingassets.NewService(teachingassets.NewMemoryStore(), onto, "gpt-image-v2")
handler := NewHandler(config.Config{Environment: "test"}, service, memory, progress, onto, assets)
routes := handler.Routes()
body := bytes.NewBufferString(`{

View File

@@ -0,0 +1,47 @@
package httpapi
import (
"encoding/json"
"net/http"
"tutor/internal/teachingassets"
"tutor/internal/workflows"
)
type generateTeachingAssetPromptRequest struct {
ConceptID string `json:"concept_id"`
AssetType workflows.AssetType `json:"asset_type"`
}
func (h Handler) generateTeachingAssetPrompt(w http.ResponseWriter, r *http.Request) {
if h.assets == nil {
writeError(w, http.StatusNotFound, "teaching assets not configured")
return
}
var req generateTeachingAssetPromptRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
prompt, err := h.assets.GeneratePrompt(teachingassets.GenerateInput{
ConceptID: req.ConceptID,
AssetType: req.AssetType,
})
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, prompt)
}
func (h Handler) getTeachingAssets(w http.ResponseWriter, _ *http.Request) {
if h.assets == nil {
writeError(w, http.StatusNotFound, "teaching assets not configured")
return
}
writeJSON(w, http.StatusOK, h.assets.Snapshot())
}

View File

@@ -0,0 +1,64 @@
package httpapi
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"tutor/internal/config"
"tutor/internal/interview"
"tutor/internal/learnermemory"
"tutor/internal/ontology"
"tutor/internal/progression"
"tutor/internal/teachingassets"
"tutor/internal/workflows"
)
func TestTeachingAssetsHTTPFlow(t *testing.T) {
memory := learnermemory.NewService(learnermemory.NewMemoryStore())
service := interview.NewService(interview.NewMemoryStore(), workflows.NewStubRunner(), memory)
progress := progression.NewService(memory)
onto := ontology.NewService(ontology.NewMemoryStore())
assets := teachingassets.NewService(teachingassets.NewMemoryStore(), onto, "gpt-image-v2")
handler := NewHandler(config.Config{Environment: "test"}, service, memory, progress, onto, assets)
routes := handler.Routes()
ingestBody := bytes.NewBufferString(`{
"title":"Backend notes",
"body":"Idempotent API retries need transactions."
}`)
ingestReq := httptest.NewRequest(http.MethodPost, "/api/v1/materials", ingestBody)
ingestRec := httptest.NewRecorder()
routes.ServeHTTP(ingestRec, ingestReq)
if ingestRec.Code != http.StatusCreated {
t.Fatalf("ingest status = %d, body = %s", ingestRec.Code, ingestRec.Body.String())
}
promptBody := bytes.NewBufferString(`{
"concept_id":"http-idempotency",
"asset_type":"diagram"
}`)
promptReq := httptest.NewRequest(http.MethodPost, "/api/v1/teaching-assets/prompts", promptBody)
promptRec := httptest.NewRecorder()
routes.ServeHTTP(promptRec, promptReq)
if promptRec.Code != http.StatusCreated {
t.Fatalf("prompt status = %d, body = %s", promptRec.Code, promptRec.Body.String())
}
var prompt teachingassets.PromptCandidate
if err := json.NewDecoder(promptRec.Body).Decode(&prompt); err != nil {
t.Fatalf("decode prompt response: %v", err)
}
if !prompt.RequiresModelIDVerification {
t.Fatal("expected verification guard")
}
getReq := httptest.NewRequest(http.MethodGet, "/api/v1/teaching-assets", nil)
getRec := httptest.NewRecorder()
routes.ServeHTTP(getRec, getReq)
if getRec.Code != http.StatusOK {
t.Fatalf("assets status = %d, body = %s", getRec.Code, getRec.Body.String())
}
}