feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
@@ -18,24 +18,28 @@ import (
|
||||
)
|
||||
|
||||
type GenerationInput struct {
|
||||
TenantID string
|
||||
Content string
|
||||
TenantID string
|
||||
Content string
|
||||
ChatDate string
|
||||
PreviousStories []PreviousStory
|
||||
}
|
||||
|
||||
type GenerationResult struct {
|
||||
Meaningful bool `json:"meaningful"`
|
||||
StoryWorthy bool `json:"story_worthy"`
|
||||
DecisionReason string `json:"decision_reason"`
|
||||
Summary string `json:"summary"`
|
||||
OpenTopic string `json:"open_topic"`
|
||||
StoryTitle string `json:"story_title"`
|
||||
StoryContent string `json:"story_content"`
|
||||
FactContent string `json:"fact_content"`
|
||||
Keywords []string `json:"keywords"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
ModelRef string `json:"-"`
|
||||
PromptVersion string `json:"-"`
|
||||
AllowStory bool `json:"-"`
|
||||
Meaningful bool `json:"meaningful"`
|
||||
StoryWorthy bool `json:"story_worthy"`
|
||||
DecisionReason string `json:"decision_reason"`
|
||||
Summary string `json:"summary"`
|
||||
OpenTopic string `json:"open_topic"`
|
||||
StoryTitle string `json:"story_title"`
|
||||
StoryContent string `json:"story_content"`
|
||||
FactContent string `json:"fact_content"`
|
||||
Keywords []string `json:"keywords"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
ModelRef string `json:"-"`
|
||||
PromptVersion string `json:"-"`
|
||||
AllowStory bool `json:"-"`
|
||||
DailyEvents []DailyEvent `json:"daily_events,omitempty"`
|
||||
Stories []EventStory `json:"stories,omitempty"`
|
||||
}
|
||||
|
||||
type Generator interface {
|
||||
@@ -114,17 +118,20 @@ func safePolicyToken(value string) string {
|
||||
}
|
||||
|
||||
type ArkGenerator struct {
|
||||
endpoint string
|
||||
apiKey string
|
||||
model string
|
||||
provider string
|
||||
temperature float64
|
||||
maxTokens int
|
||||
client *http.Client
|
||||
endpoint string
|
||||
apiKey string
|
||||
model string
|
||||
provider string
|
||||
temperature float64
|
||||
maxTokens int
|
||||
jsonMaxTokens int
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (*ArkGenerator) SupportsEventStories() bool { return true }
|
||||
|
||||
func NewArkGenerator(baseURL, apiKey, model string, timeout time.Duration) (*ArkGenerator, error) {
|
||||
return NewCompatibleGenerator("ark", baseURL, apiKey, model, 0.2, 1200, timeout)
|
||||
return NewCompatibleGenerator("ark", baseURL, apiKey, model, 0.2, 8192, timeout)
|
||||
}
|
||||
|
||||
func NewCompatibleGenerator(provider, baseURL, apiKey, model string, temperature float64, maxTokens int, timeout time.Duration) (*ArkGenerator, error) {
|
||||
@@ -158,11 +165,14 @@ func (g *ArkGenerator) Generate(ctx context.Context, input GenerationInput) (Gen
|
||||
"model": g.model,
|
||||
"messages": []map[string]string{
|
||||
{"role": "system", "content": contentValuePrompt},
|
||||
{"role": "user", "content": content},
|
||||
{"role": "user", "content": generationContent(input)},
|
||||
},
|
||||
"temperature": g.temperature,
|
||||
"max_tokens": g.maxTokens,
|
||||
}
|
||||
if g.provider == "ark" {
|
||||
requestBody["thinking"] = map[string]string{"type": "disabled"}
|
||||
}
|
||||
// JSON mode is sent only where the provider documents it or this project
|
||||
// has exercised it. Other compatible providers still receive an explicit
|
||||
// JSON-only prompt, followed by strict response extraction and validation.
|
||||
@@ -198,7 +208,8 @@ func (g *ArkGenerator) Generate(ctx context.Context, input GenerationInput) (Gen
|
||||
}
|
||||
var completion struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
FinishReason string `json:"finish_reason"`
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
@@ -206,6 +217,9 @@ func (g *ArkGenerator) Generate(ctx context.Context, input GenerationInput) (Gen
|
||||
if err = json.Unmarshal(responseBody, &completion); err != nil || len(completion.Choices) == 0 {
|
||||
return GenerationResult{}, outbox.Retryable(errors.New("content model response has no usable choice"))
|
||||
}
|
||||
if completion.Choices[0].FinishReason == "length" {
|
||||
return GenerationResult{}, outbox.Retryable(errors.New("content model result truncated"))
|
||||
}
|
||||
jsonContent := extractJSONObject(completion.Choices[0].Message.Content)
|
||||
jsonContent = strings.TrimPrefix(jsonContent, "```json")
|
||||
jsonContent = strings.TrimPrefix(jsonContent, "```")
|
||||
@@ -221,12 +235,55 @@ func (g *ArkGenerator) Generate(ctx context.Context, input GenerationInput) (Gen
|
||||
result.StoryContent = strings.TrimSpace(result.StoryContent)
|
||||
result.FactContent = strings.TrimSpace(result.FactContent)
|
||||
result.Keywords = normalizedKeywords(result.Keywords)
|
||||
if result.DailyEvents != nil {
|
||||
// Review dates separately from story editing: a past event can be about
|
||||
// ordinary life, and today's speech can describe a future plan.
|
||||
raw, marshalErr := json.Marshal(map[string]any{"chat_date": input.ChatDate, "transcript": input.Content})
|
||||
if marshalErr != nil {
|
||||
return GenerationResult{}, marshalErr
|
||||
}
|
||||
checked, checkErr := g.GenerateJSON(ctx, string(raw), dailyExtractionPrompt)
|
||||
if checkErr != nil {
|
||||
return GenerationResult{}, checkErr
|
||||
}
|
||||
var daily struct {
|
||||
Events []DailyEvent `json:"daily_events"`
|
||||
}
|
||||
if json.Unmarshal(checked, &daily) != nil || daily.Events == nil {
|
||||
return GenerationResult{}, errors.New("daily classification failed validation")
|
||||
}
|
||||
result.DailyEvents = daily.Events
|
||||
}
|
||||
if err = normalizeEvents(&result, input); err != nil {
|
||||
return GenerationResult{}, outbox.Retryable(err)
|
||||
}
|
||||
normalizeDecisionContent(&result)
|
||||
if !validGeneratedContent(result) {
|
||||
return GenerationResult{}, outbox.Retryable(errors.New("structured content failed validation"))
|
||||
}
|
||||
result.ModelRef = g.provider + ":" + g.model
|
||||
result.PromptVersion = "content-value-" + g.provider + "-json-v3"
|
||||
for _, d := range result.DailyEvents {
|
||||
if d.TimeScope != "today_actual" || strings.TrimSpace(d.Evidence) == "" || !strings.Contains(input.Content, d.Evidence) {
|
||||
return GenerationResult{}, errors.New("daily event lacks evidence of an actual event today")
|
||||
}
|
||||
}
|
||||
actual := make([]DailyEvent, 0, len(result.DailyEvents))
|
||||
for _, d := range result.DailyEvents {
|
||||
future := false
|
||||
for _, phrase := range []string{"明天", "明早", "前几天", "打算", "计划", "尚未", "还没动手", "想着", "还想", "一会儿", "以后再"} {
|
||||
if strings.Contains(d.Content, phrase) {
|
||||
future = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !future {
|
||||
actual = append(actual, d)
|
||||
}
|
||||
}
|
||||
if result.DailyEvents != nil {
|
||||
result.DailyEvents = actual
|
||||
}
|
||||
result.PromptVersion = "content-events-" + g.provider + "-json-v5"
|
||||
result.AllowStory = result.Meaningful && result.StoryWorthy
|
||||
return result, nil
|
||||
}
|
||||
@@ -269,6 +326,8 @@ func extractJSONObject(content string) string {
|
||||
|
||||
func normalizeDecisionContent(result *GenerationResult) {
|
||||
if !result.Meaningful {
|
||||
result.Stories = nil
|
||||
result.DailyEvents = nil
|
||||
result.StoryWorthy = false
|
||||
result.Summary = ""
|
||||
result.OpenTopic = ""
|
||||
@@ -279,6 +338,7 @@ func normalizeDecisionContent(result *GenerationResult) {
|
||||
return
|
||||
}
|
||||
if !result.StoryWorthy {
|
||||
result.Stories = nil
|
||||
result.StoryTitle = ""
|
||||
result.StoryContent = ""
|
||||
result.FactContent = ""
|
||||
@@ -306,19 +366,7 @@ func validGeneratedContent(result GenerationResult) bool {
|
||||
result.Confidence >= 0 && result.Confidence <= 1
|
||||
}
|
||||
|
||||
const contentValuePrompt = `你是长者聊天内容筛选与整理助手。只依据输入对话判断和整理,不补充未出现的事实,不把推断写成事实。
|
||||
|
||||
先做两级判断:
|
||||
1. meaningful:对话是否包含值得保留的信息。只有问候、寒暄、嗯啊、测试设备、重复句、零散且无法理解的短句,必须判为false并舍弃,不生成摘要。
|
||||
2. story_worthy:仅在meaningful为true时判断是否值得形成故事。具体的个人经历或回忆,且人物、时间、地点、事件、行动、感受中至少出现两类,或形成了清楚的事件脉络,才判为true。普通偏好、单个事实或简短近况可以总结,但story_worthy应为false。
|
||||
|
||||
必须只返回JSON对象,字段为meaningful、story_worthy、decision_reason、summary、keywords、open_topic、story_title、story_content、fact_content、confidence。
|
||||
- decision_reason用一句简短中文说明判断依据。
|
||||
- meaningful=false时,除decision_reason和confidence外,其余文本必须为空字符串,keywords必须为空数组,story_worthy必须为false。
|
||||
- meaningful=true时,summary概括有意义内容,keywords为1至8个简短关键词。只有存在明确、自然且尚未聊完的线索时,open_topic才给出一个温和的后续方向;没有合适线索时必须为空字符串,不要为了填字段编造问题。
|
||||
- story_worthy=false时,story_title、story_content、fact_content必须为空字符串。
|
||||
- story_worthy=true时,story_title和story_content忠实整理原话,fact_content只写对话中可核对的事实。
|
||||
- confidence取0到1。`
|
||||
const contentValuePrompt = eventExtractionPrompt
|
||||
|
||||
func normalizedKeywords(values []string) []string {
|
||||
result := make([]string, 0, min(len(values), 8))
|
||||
|
||||
Reference in New Issue
Block a user