Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ ONGRID_OPENAI_API_KEY=
ONGRID_OPENAI_MODEL=gpt-4o
ONGRID_OPENAI_BASE_URL=

# --- LLM Wiki (auditable Raw -> compiled Wiki knowledge layer) ---
# Vector search writes to the shared Qdrant (ONGRID_QDRANT_URL, default
# http://qdrant:6333) once ONGRID_EMBEDDING_* is configured; without an
# embedding key the Wiki runs lexical-only.
ONGRID_LLM_WIKI_ENABLED=true
ONGRID_LLM_WIKI_DIR=/var/lib/ongrid/llm-wiki
ONGRID_LLM_WIKI_TIMEOUT_SECONDS=600

# --- Cloud-side Prometheus (ADR-009) ---
# When ONGRID_PROM_ENABLED=true the manager forwards push_prom_samples to
# remote_write, and the AI agent gains a query_promql tool. Leave false
Expand Down
171 changes: 171 additions & 0 deletions api/manager/knowledge/v1/knowledge.proto
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
syntax = "proto3";

package ongrid.manager.knowledge.v1;
option go_package = "github.com/ongridio/ongrid/api/gen/manager/knowledge/v1;knowledgev1";

Expand All @@ -24,3 +25,173 @@ message UpdateRepositoryRequest {
string branch = 2;
string description = 3;
}

// ---------------------------------------------------------------------------
// LLM Wiki
//
// The compiled Wiki: mirrored raw sources, the generated pages of the active
// build and the compile jobs that produce them. Every route answers the shared
// `{code, message, data}` HTTP envelope, so only the `data` payload is modelled
// here. Node ids are opaque to clients: they encode the layer and the
// store-relative path and must be sent back unchanged.
// ---------------------------------------------------------------------------

// One mirrored raw source.
message LLMWikiSource {
string id = 1;
string source_key = 2;
string source_type = 3;
string raw_path = 4;
string current_version_id = 5;
string status = 6; // pending | succeeded | stale | failed
string updated_at = 7;
}

// One compile job. A tenant has at most one pending or running job.
message LLMWikiJob {
string id = 1;
string status = 2; // pending | running | succeeded | skipped | failed | cancelled
string stage = 3;
string error = 4;
string created_at = 5;
string updated_at = 6;
}

// One node of the Wiki tree: a raw file, a generated page or a folder.
message LLMWikiTreeNode {
string id = 1;
string parent_id = 2;
string layer = 3; // raw | wiki
string kind = 4; // folder | file
string name = 5;
string relative_path = 6;
bool has_children = 7;
int64 child_count = 8;
int64 document_count = 9;
string source_id = 10;
string page_id = 11;
string page_type = 12;
string status = 13;
string updated_at = 14;
}

// One published Wiki page.
message LLMWikiSearchHit {
string layer = 1;
string page_type = 2;
string page_id = 3;
string source_version_id = 4;
string title = 5;
string preview = 6;
double score = 7;
string matched_node = 8;
}

// GET /v1/knowledge/llm-wiki/tree?layer=&parent_id=
message LLMWikiTreeRequest {
string layer = 1; // raw | wiki | empty for both layers
string parent_id = 2; // empty for the layer root
}
message LLMWikiTreeResponse {
repeated LLMWikiTreeNode items = 1;
int64 total = 2;
int64 document_count = 3;
}

// GET /v1/knowledge/llm-wiki/nodes/{id} answers with the tree node flattened
// next to the body and its metadata, because the Go DTO embeds TreeNode.
// Metadata is a JSON object in `metadata_json`.
message LLMWikiNodeRequest {
string id = 1;
}
message LLMWikiNodeResponse {
string id = 1;
string parent_id = 2;
string layer = 3;
string kind = 4;
string name = 5;
string relative_path = 6;
bool has_children = 7;
int64 child_count = 8;
int64 document_count = 9;
string source_id = 10;
string page_id = 11;
string page_type = 12;
string status = 13;
string updated_at = 14;
string content = 15;
string metadata_json = 16;
}

// GET /v1/knowledge/llm-wiki/nodes/{id}/preview streams the original bytes of a
// raw PDF or DOCX instead of the JSON envelope.
message LLMWikiPreviewRequest {
string id = 1;
}

// DELETE /v1/knowledge/llm-wiki/nodes/{id} removes a raw source, the pages
// generated from it and its index entries. Generated pages cannot be deleted
// individually: they belong to an immutable build.
message LLMWikiDeleteRequest {
string id = 1;
}
message LLMWikiDeleteResponse {
bool deleted = 1;
}

// GET /v1/knowledge/llm-wiki/sources?status=&limit=
message LLMWikiSourcesRequest {
string status = 1;
int32 limit = 2;
}
message LLMWikiSourcesResponse {
repeated LLMWikiSource items = 1;
int64 total = 2;
}

// GET /v1/knowledge/llm-wiki/search?q=&limit= searches published Wiki pages
// only; knowledge-base path and tag filters are not supported here.
message LLMWikiSearchRequest {
string q = 1;
int32 limit = 2;
}
message LLMWikiSearchResponse {
repeated LLMWikiSearchHit items = 1;
int64 total = 2;
}

// GET /v1/knowledge/llm-wiki/jobs?limit=
message LLMWikiJobsRequest {
int32 limit = 1;
}
message LLMWikiJobsResponse {
repeated LLMWikiJob items = 1;
int64 total = 2;
}

// POST /v1/knowledge/llm-wiki/sync projects the organization knowledge base
// (manual and upload documents) into the Wiki raw tree. It is refused with a
// conflict while a compile job is pending or running.
message LLMWikiSyncResponse {
int64 created = 1;
int64 updated = 2;
int64 unchanged = 3;
int64 deleted = 4;
int64 total = 5;
}

// POST /v1/knowledge/llm-wiki/compile queues one compilation and answers 202
// with an LLMWikiJob as the payload. An empty source_ids compiles the whole
// corpus; otherwise only the listed sources are recompiled and the pages that do
// not depend on them are inherited from the active build.
message LLMWikiCompileRequest {
bool force = 1;
repeated string source_ids = 2;
}

// POST /v1/knowledge/llm-wiki/jobs/{id}/retry re-queues a failed or cancelled
// job; POST /v1/knowledge/llm-wiki/jobs/{id}/cancel requests cancellation of a
// pending or running one. Both answer with the updated LLMWikiJob as the payload.
message LLMWikiJobRequest {
string id = 1;
}
58 changes: 49 additions & 9 deletions cmd/ongrid/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ import (
managerbizimbridgeslack "github.com/ongridio/ongrid/internal/manager/biz/imbridge/provider/slack"
managerbizimbridgetelegram "github.com/ongridio/ongrid/internal/manager/biz/imbridge/provider/telegram"
managerbizknowledge "github.com/ongridio/ongrid/internal/manager/biz/knowledge"
managerbizllmwiki "github.com/ongridio/ongrid/internal/manager/biz/knowledge/llm_wiki"
managerbizmarketplace "github.com/ongridio/ongrid/internal/manager/biz/marketplace"
managerbizmcp "github.com/ongridio/ongrid/internal/manager/biz/mcp"
managerbizmonitor "github.com/ongridio/ongrid/internal/manager/biz/monitor"
Expand All @@ -131,6 +132,8 @@ import (
manageraiopsdata "github.com/ongridio/ongrid/internal/manager/data/aiops/store"
managerapprovaldata "github.com/ongridio/ongrid/internal/manager/data/approval/store"
managerimbridgedata "github.com/ongridio/ongrid/internal/manager/data/imbridge/store"
managerllmwikidata "github.com/ongridio/ongrid/internal/manager/data/knowledge/llm_wiki"
managerllmwikistore "github.com/ongridio/ongrid/internal/manager/data/knowledge/llm_wiki/store"
managerknowledgedata "github.com/ongridio/ongrid/internal/manager/data/knowledge/store"
managermarketplacedata "github.com/ongridio/ongrid/internal/manager/data/marketplace/store"
managermcpdata "github.com/ongridio/ongrid/internal/manager/data/mcp/store"
Expand Down Expand Up @@ -194,6 +197,7 @@ import (
managersvcedge "github.com/ongridio/ongrid/internal/manager/service/edge"
managersvcfb "github.com/ongridio/ongrid/internal/manager/service/frontierbound"
managersvck8s "github.com/ongridio/ongrid/internal/manager/service/k8s"
managersvcknowledge "github.com/ongridio/ongrid/internal/manager/service/knowledge"
managersvcmetric "github.com/ongridio/ongrid/internal/manager/service/metric"
managersvcprom "github.com/ongridio/ongrid/internal/manager/service/prometheus"
managersvcsystemhealth "github.com/ongridio/ongrid/internal/manager/service/systemhealth"
Expand Down Expand Up @@ -301,6 +305,7 @@ func main() {
manageraudtdata.Migrate,
managerreportdata.Migrate,
managerflowdata.Migrate,
managerllmwikistore.Migrate,
managerpacketcapturedata.Migrate,
); err != nil {
log.Error("run migrations", slog.Any("err", err))
Expand Down Expand Up @@ -1501,23 +1506,49 @@ func main() {
if qdrantURL == "" {
qdrantURL = "http://qdrant:6333"
}
qdrantClient := qdrantx.New(qdrantURL, log.With(slog.String("comp", "qdrant")))
var maybeEmbedder embedding.Embedder
if embErr != nil {
log.Warn("knowledge: embedder unavailable — reads enabled, writes disabled",
slog.Any("err", embErr))
} else {
maybeEmbedder = embedder
}
var llmWikiUC *managerbizllmwiki.Usecase
if cfg.LLMWiki.Enabled {
wikiRoot := strings.TrimSpace(cfg.LLMWiki.Dir)
wikiFiles, err := managerbizllmwiki.NewFileStore(wikiRoot)
if err != nil {
log.Error("llm wiki: file store failed", slog.Any("err", err))
} else if ensureErr := wikiFiles.Ensure(rootCtx); ensureErr != nil {
log.Error("llm wiki: initialize file tree failed", slog.Any("err", ensureErr))
} else {
wikiStore, openErr := managerllmwikidata.Open(rootCtx, db, qdrantClient, maybeEmbedder, embDim, log.With(slog.String("comp", "llmwiki-db")))
if openErr != nil {
log.Error("llm wiki: store open failed", slog.Any("err", openErr))
} else {
wikiRepo := wikiStore.Repository()
wikiIndexer := wikiStore.SearchIndex
// Bind Wiki compilation to the configured LLM model. Provider/Model
// are left empty so the shared LLM router follows the user's default.
wikiSummarizer := managerbizllmwiki.NewLLMAdapter(llmClient, "", "", cfg.OpenAI.Model)
llmWikiUC, err = managerbizllmwiki.NewWithUsageRecorder(rootCtx, wikiRepo, wikiFiles, wikiSummarizer, wikiIndexer, log.With(slog.String("comp", "llmwiki")), managersvcknowledge.NewLLMWikiUsageRecorder(aiopsRepo), managerbizllmwiki.CompileTriggerOption{Owner: "ongrid-manager", Timeout: time.Duration(cfg.LLMWiki.TimeoutSeconds) * time.Second})
if err != nil {
log.Error("llm wiki: usecase failed", slog.Any("err", err))
}
}
}
}
var knowledgeUC *managerbizknowledge.Usecase
var knowledgeSearcher *managersvcknowledge.HybridSearcher
{
qdrantClient := qdrantx.New(qdrantURL, log.With(slog.String("comp", "qdrant")))
// Build with a nil embedder when one isn't configured — the
// usecase exposes read paths (ListDocs/Repos/GetDoc/ListPaths)
// and gates write paths (CreateManualDoc/Sync/Search) on
// embed != nil so the SPA's 知识库 / 代码仓库 pages render on
// fresh install instead of 404'ing. Operator configures
// ONGRID_EMBEDDING_API_KEY later → writes unblock without
// restart-of-stack (only the manager needs the key on boot).
var maybeEmbedder embedding.Embedder
if embErr != nil {
log.Warn("knowledge: embedder unavailable — reads enabled, writes disabled",
slog.Any("err", embErr))
} else {
maybeEmbedder = embedder
}
uc, kErr := managerbizknowledge.New(rootCtx, knowledgeRepo, qdrantClient, maybeEmbedder,
os.Getenv("ONGRID_KNOWLEDGE_REPO_DIR"),
log.With(slog.String("comp", "knowledge")))
Expand All @@ -1526,7 +1557,8 @@ func main() {
} else {
knowledgeUC = uc
go knowledgeUC.RunAutoSync(rootCtx)
toolsReg.SetKnowledgeSearcher(knowledgeUC)
knowledgeSearcher = managersvcknowledge.NewHybridSearcher(knowledgeUC, llmWikiUC)
toolsReg.SetKnowledgeSearcher(knowledgeSearcher)
apmService.WithSourceRevisions(knowledgeUC)
// GitHub-PAT-via-GIT_ASKPASS resolver wiring
// removed. SSH-style repos use ssh_identities; HTTPS auth
Expand Down Expand Up @@ -2044,6 +2076,14 @@ func main() {
var knowledgeHandler *managerserverknowledge.Handler
if knowledgeUC != nil {
knowledgeHandler = managerserverknowledge.NewHandler(knowledgeUC)
knowledgeHandler.SetSearchService(knowledgeSearcher)
knowledgeHandler.SetAuthz(authzMW)
}
if llmWikiUC != nil {
if knowledgeHandler == nil {
knowledgeHandler = managerserverknowledge.NewHandler(nil)
}
knowledgeHandler.SetLLMWikiService(llmWikiUC)
knowledgeHandler.SetAuthz(authzMW)
}

Expand Down
6 changes: 6 additions & 0 deletions db/migrations/20260918100000_add_llm_wiki_tables.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
DROP TABLE IF EXISTS wiki_lexical;
DROP TABLE IF EXISTS wiki_build_pages;
DROP TABLE IF EXISTS wiki_builds;
DROP TABLE IF EXISTS wiki_compile_jobs;
DROP TABLE IF EXISTS wiki_source_versions;
DROP TABLE IF EXISTS wiki_sources;
Loading