97 lines
2.8 KiB
Go
97 lines
2.8 KiB
Go
|
|
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))
|
||
|
|
}
|