feat(gateway): automation rule engine and reminder scheduler
- engine: 5-field cron parser (*/n, ranges, lists) firing at most once per minute; device-state triggers with edge detection; manual trigger via POST /automation/rules/:id/trigger; actions run through the AI pipeline so rules can call any tool (IoT control, search, ...) - reminders: due-scan scheduler delivering via WebSocket broadcast, optional LLM-polished reminder text with plain-title fallback; Reminder gains Done flag with ListDue/MarkDone - tests: cron matching and validation, once-per-minute dedup, device edge triggering, manual trigger, reminder delivery
This commit is contained in:
1 parent
9f30cdd086
commit
d80710d85a
7 files changed
+679
-2
No files matched your search
@@ -43,6 +43,7 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/persona"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/subsession"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/api"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/engine"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/store"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/gateway/ws"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/services/memory"
|
||||
@@ -306,6 +307,35 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
// === 12.7 Automation engine ===
|
||||
autoEngine := engine.New(
|
||||
automationSourceAdapter{gwDeps.Automation},
|
||||
func(ctx context.Context, sessionID, userID, message string) {
|
||||
tokenCh, _, err := orch.ProcessInput(ctx, sessionID, userID, message)
|
||||
if err != nil {
|
||||
logger.Infof("[automation] action failed: %v", err)
|
||||
return
|
||||
}
|
||||
for range tokenCh {
|
||||
}
|
||||
},
|
||||
iotQuerierAdapter{reg},
|
||||
30*time.Second,
|
||||
)
|
||||
autoEngine.Start(context.Background())
|
||||
gwDeps.AutomationEng = autoEngine
|
||||
|
||||
// === 12.8 Reminder scheduler ===
|
||||
remSched := engine.NewReminderScheduler(
|
||||
gwDeps.Reminders,
|
||||
func(userID, title, body string) {
|
||||
wsHub.Broadcast(map[string]any{"type": "reminder", "title": title, "content": body})
|
||||
},
|
||||
&memory.LLMAdapterBridge{Adapter: llmSelector.DefaultAdapter()},
|
||||
30*time.Second,
|
||||
)
|
||||
remSched.Start(context.Background())
|
||||
|
||||
apiRouter := api.New(gwDeps)
|
||||
|
||||
wsMux := http.NewServeMux()
|
||||
@@ -394,6 +424,32 @@ func (a *perfAdapter) History() []map[string]any {
|
||||
return result
|
||||
}
|
||||
|
||||
// automationSourceAdapter adapts api.AutomationStore to engine.RuleSource.
|
||||
type automationSourceAdapter struct{ s *api.AutomationStore }
|
||||
|
||||
func (a automationSourceAdapter) List() []engine.Rule {
|
||||
rules := a.s.List()
|
||||
out := make([]engine.Rule, len(rules))
|
||||
for i, r := range rules {
|
||||
out[i] = engine.Rule{
|
||||
ID: r.ID, Name: r.Name, Trigger: r.Trigger,
|
||||
Condition: r.Condition, Action: r.Action, Enabled: r.Enabled,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// iotQuerierAdapter adapts the plugin registry to engine.DeviceQuerier.
|
||||
type iotQuerierAdapter struct{ reg *plugin.Registry }
|
||||
|
||||
func (a iotQuerierAdapter) ListDevices(ctx context.Context) ([]plugin.Device, error) {
|
||||
iot := a.reg.GetIoTProvider()
|
||||
if iot == nil {
|
||||
return nil, fmt.Errorf("no iot provider")
|
||||
}
|
||||
return iot.ListDevices(ctx)
|
||||
}
|
||||
|
||||
// chatPersisterAdapter adapts store.ChatStore to aiCoreCtx.Persister.
|
||||
type chatPersisterAdapter struct{ s *store.ChatStore }
|
||||
|
||||
|
||||
Reference in new issue
Block a user