- internal/hub: project side of the frozen ask-contract. GET /capabilities, read-only POST /ask answerer (no LLM, <3s), and the outbound POST /notify client. - internal/apprise: daily-summary delivery through the house Apprise API. - internal/summary: deterministic plain-text daily summary builder. - internal/store: CountByCategory, MessagesByCategory, and summary CRUD. - internal/server: /capabilities and /ask (bearer-token protected), plus the Reports screen (/reports, build, send, delete). - main.go: -summary, -summary-send, and -notify CLI entry points for n8n. Co-Authored-By: Paperclip <noreply@paperclip.ing>
94 lines
3.4 KiB
Go
94 lines
3.4 KiB
Go
// Package hub implements the project side of the Hub contract
|
|
// (central_ai_llm_master/docs/ask-contract.md) and the outbound notify call.
|
|
//
|
|
// The contract is one-directional by design: the Hub asks a project about its
|
|
// own data and the project answers. A project never learns the Hub's data model
|
|
// and the Hub never learns the project's. The project endpoints are
|
|
// GET /capabilities and POST /ask (read only); POST /notify is the
|
|
// project-to-Hub direction.
|
|
//
|
|
// Phase 7 (FR-12). Answers are computed from SQLite; /ask performs no writes.
|
|
package hub
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"gitea.lab.audasmedia.com.au/sam/email_agent/internal/store"
|
|
)
|
|
|
|
// ProjectName is the value the Hub matches in routing.md.
|
|
const ProjectName = "email_agent"
|
|
|
|
// Capabilities is the GET /capabilities reply. The Hub reads it to route a
|
|
// question to this project. Keep the topic list short and plain; it holds no
|
|
// private data.
|
|
type Capabilities struct {
|
|
Project string `json:"project"`
|
|
Topics []string `json:"topics"`
|
|
People []string `json:"people"`
|
|
}
|
|
|
|
// DefaultCapabilities returns the topics this service can answer. "email",
|
|
// "inbox", "mail", and "message" match the routing.md topic map. Read/unread is
|
|
// deliberately absent: the service does not track read state.
|
|
func DefaultCapabilities() Capabilities {
|
|
return Capabilities{
|
|
Project: ProjectName,
|
|
Topics: []string{"email", "inbox", "mail", "message"},
|
|
People: []string{"sam"},
|
|
}
|
|
}
|
|
|
|
// AskRequest is the POST /ask body (ask-contract.md).
|
|
type AskRequest struct {
|
|
Question string `json:"question"`
|
|
Asker string `json:"asker"`
|
|
Person string `json:"person,omitempty"`
|
|
Source string `json:"source,omitempty"`
|
|
}
|
|
|
|
// AskResponse is the POST /ask reply. A nil Answer means the project cannot
|
|
// answer and Reason explains why. The shape is frozen by the contract.
|
|
type AskResponse struct {
|
|
Answer *string `json:"answer"`
|
|
Data map[string]any `json:"data,omitempty"`
|
|
Expires string `json:"expires,omitempty"`
|
|
Reason string `json:"reason,omitempty"`
|
|
}
|
|
|
|
// answerText is a small helper so the pointer handling stays in one place.
|
|
func answerText(s string) *string { return &s }
|
|
|
|
// cannotAnswer builds the contract's "cannot answer" reply.
|
|
func cannotAnswer(reason string) AskResponse {
|
|
return AskResponse{Answer: nil, Reason: reason}
|
|
}
|
|
|
|
// expiresIn formats an RFC3339 expiry used by data-bearing answers. An inbox
|
|
// answer goes stale quickly because mail arrives and is triaged.
|
|
func expiresIn(now time.Time, d time.Duration) string {
|
|
return now.UTC().Add(d).Format(time.RFC3339)
|
|
}
|
|
|
|
// NotifyRequest is the project-to-Hub POST /notify body (ask-contract.md). The
|
|
// Hub may use it to speak or notify.
|
|
type NotifyRequest struct {
|
|
From string `json:"from"`
|
|
Level string `json:"level"`
|
|
Message string `json:"message"`
|
|
Person string `json:"person,omitempty"`
|
|
}
|
|
|
|
// jsonBody marshals a notify payload, kept small and deterministic for tests.
|
|
func jsonBody(v any) ([]byte, error) { return json.Marshal(v) }
|
|
|
|
// Reader is the read-only data surface the answerer uses. *store.Store
|
|
// satisfies it; tests can supply a fake. Keeping it an interface makes the
|
|
// read-only guarantee explicit and easy to assert.
|
|
type Reader interface {
|
|
CountByCategory(ctx context.Context, folder string) ([]store.CategoryCount, error)
|
|
MessagesByCategory(ctx context.Context, folder string, categories []string, limit int) ([]store.MessageListItem, error)
|
|
}
|