feat: add ontology material ingestion
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"tutor/internal/config"
|
||||
"tutor/internal/interview"
|
||||
"tutor/internal/learnermemory"
|
||||
"tutor/internal/ontology"
|
||||
"tutor/internal/progression"
|
||||
"tutor/internal/workflows"
|
||||
)
|
||||
@@ -18,7 +19,8 @@ func TestDiagnosticHTTPFlow(t *testing.T) {
|
||||
memory := learnermemory.NewService(learnermemory.NewMemoryStore())
|
||||
service := interview.NewService(interview.NewMemoryStore(), workflows.NewStubRunner(), memory)
|
||||
progress := progression.NewService(memory)
|
||||
handler := NewHandler(config.Config{Environment: "test", ModelKey: "deepseek-v4-flash"}, service, memory, progress)
|
||||
onto := ontology.NewService(ontology.NewMemoryStore())
|
||||
handler := NewHandler(config.Config{Environment: "test", ModelKey: "deepseek-v4-flash"}, service, memory, progress, onto)
|
||||
routes := handler.Routes()
|
||||
|
||||
createBody := bytes.NewBufferString(`{
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"tutor/internal/config"
|
||||
"tutor/internal/interview"
|
||||
"tutor/internal/learnermemory"
|
||||
"tutor/internal/ontology"
|
||||
"tutor/internal/progression"
|
||||
)
|
||||
|
||||
@@ -15,6 +16,7 @@ type Handler struct {
|
||||
diagnostic *interview.Service
|
||||
memory *learnermemory.Service
|
||||
progress *progression.Service
|
||||
ontology *ontology.Service
|
||||
}
|
||||
|
||||
func NewHandler(
|
||||
@@ -22,12 +24,14 @@ func NewHandler(
|
||||
diagnostic *interview.Service,
|
||||
memory *learnermemory.Service,
|
||||
progress *progression.Service,
|
||||
ontology *ontology.Service,
|
||||
) Handler {
|
||||
return Handler{
|
||||
cfg: cfg,
|
||||
diagnostic: diagnostic,
|
||||
memory: memory,
|
||||
progress: progress,
|
||||
ontology: ontology,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +44,8 @@ func (h Handler) Routes() http.Handler {
|
||||
mux.HandleFunc("GET /api/v1/learners/{userID}/memory", h.getLearnerMemory)
|
||||
mux.HandleFunc("GET /api/v1/learners/{userID}/readiness-map", h.getReadinessMap)
|
||||
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)
|
||||
return mux
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"tutor/internal/config"
|
||||
"tutor/internal/interview"
|
||||
"tutor/internal/learnermemory"
|
||||
"tutor/internal/ontology"
|
||||
"tutor/internal/progression"
|
||||
"tutor/internal/workflows"
|
||||
)
|
||||
@@ -21,7 +22,8 @@ func TestHealth(t *testing.T) {
|
||||
memory := learnermemory.NewService(learnermemory.NewMemoryStore())
|
||||
service := interview.NewService(interview.NewMemoryStore(), workflows.NewStubRunner(), memory)
|
||||
progress := progression.NewService(memory)
|
||||
handler := NewHandler(cfg, service, memory, progress)
|
||||
onto := ontology.NewService(ontology.NewMemoryStore())
|
||||
handler := NewHandler(cfg, service, memory, progress, onto)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
48
internal/httpapi/ontology.go
Normal file
48
internal/httpapi/ontology.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"tutor/internal/ontology"
|
||||
)
|
||||
|
||||
type ingestMaterialRequest struct {
|
||||
Title string `json:"title"`
|
||||
SourceType string `json:"source_type"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
func (h Handler) ingestMaterial(w http.ResponseWriter, r *http.Request) {
|
||||
if h.ontology == nil {
|
||||
writeError(w, http.StatusNotFound, "ontology not configured")
|
||||
return
|
||||
}
|
||||
|
||||
var req ingestMaterialRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.ontology.Ingest(ontology.IngestInput{
|
||||
Title: req.Title,
|
||||
SourceType: req.SourceType,
|
||||
Body: req.Body,
|
||||
})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, result)
|
||||
}
|
||||
|
||||
func (h Handler) getOntology(w http.ResponseWriter, _ *http.Request) {
|
||||
if h.ontology == nil {
|
||||
writeError(w, http.StatusNotFound, "ontology not configured")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, h.ontology.Snapshot())
|
||||
}
|
||||
54
internal/httpapi/ontology_test.go
Normal file
54
internal/httpapi/ontology_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
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/workflows"
|
||||
)
|
||||
|
||||
func TestOntologyHTTPFlow(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())
|
||||
handler := NewHandler(config.Config{Environment: "test"}, service, memory, progress, onto)
|
||||
routes := handler.Routes()
|
||||
|
||||
body := bytes.NewBufferString(`{
|
||||
"title":"Backend interview notes",
|
||||
"source_type":"markdown",
|
||||
"body":"Idempotent API retries need transactions. Cache invalidation uses TTL tradeoffs."
|
||||
}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/materials", body)
|
||||
rec := httptest.NewRecorder()
|
||||
routes.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("ingest status = %d, body = %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
var result ontology.IngestResult
|
||||
if err := json.NewDecoder(rec.Body).Decode(&result); err != nil {
|
||||
t.Fatalf("decode ingest response: %v", err)
|
||||
}
|
||||
if len(result.Snapshot.Concepts) == 0 {
|
||||
t.Fatal("expected ontology concepts")
|
||||
}
|
||||
|
||||
getReq := httptest.NewRequest(http.MethodGet, "/api/v1/ontology", nil)
|
||||
getRec := httptest.NewRecorder()
|
||||
routes.ServeHTTP(getRec, getReq)
|
||||
|
||||
if getRec.Code != http.StatusOK {
|
||||
t.Fatalf("ontology status = %d, body = %s", getRec.Code, getRec.Body.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user