Files
tutor-service/internal/teachingassets/service.go

97 lines
2.8 KiB
Go
Raw Normal View History

2026-04-26 17:54:23 +09:00
package teachingassets
import (
"errors"
"fmt"
"strings"
"sync/atomic"
"time"
"tutor/internal/ontology"
"tutor/internal/workflows"
)
type Service struct {
store Store
ontology *ontology.Service
imageModelKey string
ids atomic.Uint64
}
func NewService(store Store, ontology *ontology.Service, imageModelKey string) *Service {
return &Service{
store: store,
ontology: ontology,
imageModelKey: imageModelKey,
}
}
func (s *Service) GeneratePrompt(input GenerateInput) (PromptCandidate, error) {
if strings.TrimSpace(input.ConceptID) == "" {
return PromptCandidate{}, errors.New("concept_id is required")
}
assetType := input.AssetType
if assetType == "" {
assetType = workflows.AssetDiagram
}
concept, err := s.findConcept(input.ConceptID)
if err != nil {
return PromptCandidate{}, err
}
if len(concept.Evidence) == 0 {
return PromptCandidate{}, errors.New("concept has no source evidence")
}
prompt := PromptCandidate{
ID: s.nextID("asset-prompt"),
Concept: concept.Concept,
AssetType: assetType,
Prompt: buildPrompt(concept.Concept, assetType),
SourceEvidence: append([]workflows.EvidenceRef(nil), concept.Evidence...),
ModelKey: s.imageModelKey,
RequiresModelIDVerification: true,
ReviewState: ReviewCandidate,
CreatedAt: time.Now().UTC(),
}
return s.store.SavePrompt(prompt)
}
func (s *Service) Snapshot() Snapshot {
return s.store.Snapshot()
}
func (s *Service) findConcept(id string) (ontology.ConceptCandidate, error) {
if s.ontology == nil {
return ontology.ConceptCandidate{}, errors.New("ontology not configured")
}
snapshot := s.ontology.Snapshot()
for _, concept := range snapshot.Concepts {
if concept.Concept.ID == id {
return concept, nil
}
}
return ontology.ConceptCandidate{}, errors.New("concept not found")
}
func buildPrompt(concept workflows.ConceptRef, assetType workflows.AssetType) string {
switch assetType {
case workflows.AssetLessonSlice:
return "Create a concise slide-like lesson slice explaining " + concept.Label +
" for a backend developer interview, with one example and one pitfall."
case workflows.AssetWorksheet:
return "Create a worksheet for practicing " + concept.Label +
" with short prompts, answer space, and a rubric."
case workflows.AssetInterviewCard:
return "Create an interview explanation card for " + concept.Label +
" with definition, production tradeoff, and follow-up question."
default:
return "Create a clear technical diagram explaining " + concept.Label +
" for a backend developer interview, grounded in the provided source evidence."
}
}
func (s *Service) nextID(prefix string) string {
return fmt.Sprintf("%s-%d", prefix, s.ids.Add(1))
}