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)) }