Add Calibrate AI Control UI and fleet LLM backend wiring.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Operators toggle Logic gates vs AI Control on Settings, refresh local Ollama models, and save ai_endpoint settings via Calibrate PUT; server scheduler and agent snapshot/command paths support stateless 60s fleet decisions.
This commit is contained in:
98
server/internal/db/ai_decisions.go
Normal file
98
server/internal/db/ai_decisions.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// AIDecisionRecord is one persisted fleet AI decision cycle.
|
||||
type AIDecisionRecord struct {
|
||||
ID int64 `json:"id"`
|
||||
AgentID string `json:"agent_id"`
|
||||
PromptHash string `json:"prompt_hash"`
|
||||
Response string `json:"response"`
|
||||
CommandsExecuted string `json:"commands_executed"`
|
||||
Timestamp string `json:"ts"`
|
||||
}
|
||||
|
||||
func (d *Database) ensureAIDecisionsTable() error {
|
||||
_, err := d.Exec(`CREATE TABLE IF NOT EXISTS ai_decisions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
agent_id TEXT NOT NULL,
|
||||
prompt_hash TEXT NOT NULL DEFAULT '',
|
||||
response TEXT NOT NULL DEFAULT '',
|
||||
commands_executed TEXT NOT NULL DEFAULT '',
|
||||
ts DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = d.Exec(`CREATE INDEX IF NOT EXISTS idx_ai_decisions_agent ON ai_decisions(agent_id)`)
|
||||
_, _ = d.Exec(`CREATE INDEX IF NOT EXISTS idx_ai_decisions_ts ON ai_decisions(ts)`)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertAIDecision logs one fleet AI decision cycle.
|
||||
func (d *Database) InsertAIDecision(agentID, promptHash, response, commandsExecuted string) error {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
if err := d.ensureAIDecisionsTable(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := d.Exec(
|
||||
`INSERT INTO ai_decisions (agent_id, prompt_hash, response, commands_executed) VALUES (?, ?, ?, ?)`,
|
||||
agentID, promptHash, response, commandsExecuted,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListAIDecisions returns the most recent decisions for an agent (or all agents when agentID empty).
|
||||
func (d *Database) ListAIDecisions(agentID string, limit int) ([]AIDecisionRecord, error) {
|
||||
if d == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err := d.ensureAIDecisionsTable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
if limit > 500 {
|
||||
limit = 500
|
||||
}
|
||||
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
agentID = strings.TrimSpace(agentID)
|
||||
if agentID != "" {
|
||||
rows, err = d.Query(
|
||||
`SELECT id, agent_id, prompt_hash, response, commands_executed, ts
|
||||
FROM ai_decisions WHERE agent_id = ? ORDER BY id DESC LIMIT ?`,
|
||||
agentID, limit,
|
||||
)
|
||||
} else {
|
||||
rows, err = d.Query(
|
||||
`SELECT id, agent_id, prompt_hash, response, commands_executed, ts
|
||||
FROM ai_decisions ORDER BY id DESC LIMIT ?`,
|
||||
limit,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list ai decisions: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]AIDecisionRecord, 0, limit)
|
||||
for rows.Next() {
|
||||
var rec AIDecisionRecord
|
||||
var ts string
|
||||
if err := rows.Scan(&rec.ID, &rec.AgentID, &rec.PromptHash, &rec.Response, &rec.CommandsExecuted, &ts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rec.Timestamp = ts
|
||||
out = append(out, rec)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user