package rag
import (
"context"
"fmt"
"time"
"gitcode-mcp/internal/cache"
"gitcode-mcp/internal/progress"
)
type StatusRequest struct {
RepoID string
ProfileID string
ChunkPolicyID string
LanguagePolicyID string
DocumentInstructionID string
QueryInstructionID string
ActiveJob *JobStatus
Service *ServiceStatus
}
type StatusResult struct {
RepoID string `json:"repo_id"`
Status string `json:"status"`
Provider ProviderStatus `json:"provider"`
Namespace NamespaceStatus `json:"namespace"`
Coverage CoverageStatus `json:"coverage"`
ActiveJob *JobStatus `json:"active_job,omitempty"`
LastRun *RunStatus `json:"last_run,omitempty"`
Service *ServiceStatus `json:"service,omitempty"`
FailureClass string `json:"failure_class,omitempty"`
Message string `json:"message,omitempty"`
GeneratedAt time.Time `json:"generated_at"`
}
type ProviderStatus struct {
ProfileID string `json:"profile_id,omitempty"`
ProviderID string `json:"provider_id,omitempty"`
ProviderType string `json:"provider_type,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Model string `json:"model,omitempty"`
Revision string `json:"revision,omitempty"`
Dimensions int `json:"dimensions,omitempty"`
BatchSize int `json:"batch_size,omitempty"`
Ready bool `json:"ready"`
ErrorClass string `json:"error_class,omitempty"`
Message string `json:"message,omitempty"`
}
type NamespaceStatus struct {
ID string `json:"id,omitempty"`
Exists bool `json:"exists"`
Current bool `json:"current"`
}
type CoverageStatus struct {
TotalChunks int `json:"total_chunks"`
EmbeddedChunks int `json:"embedded_chunks"`
MissingChunks int `json:"missing_chunks"`
StaleChunks int `json:"stale_chunks"`
FailedChunks int `json:"failed_chunks"`
SkippedChunks int `json:"skipped_chunks"`
ContentGeneration int64 `json:"content_generation,omitempty"`
CoveredGeneration int64 `json:"covered_generation,omitempty"`
GenerationTracked bool `json:"generation_tracked,omitempty"`
}
type JobStatus struct {
ID string `json:"id"`
Type string `json:"type,omitempty"`
RepoID string `json:"repo_id,omitempty"`
ProfileID string `json:"profile_id,omitempty"`
Status string `json:"status"`
Steps int `json:"steps,omitempty"`
Completed int `json:"completed,omitempty"`
Error string `json:"error,omitempty"`
Progress []progress.Event `json:"progress,omitempty"`
}
type ServiceStatus struct {
Status string `json:"status"`
Running bool `json:"running"`
PID int `json:"pid,omitempty"`
SocketPresent bool `json:"socket_present"`
SocketPath string `json:"socket_path,omitempty"`
Message string `json:"message,omitempty"`
}
type RunStatus struct {
ID string `json:"id"`
NamespaceID string `json:"namespace_id"`
ProfileID string `json:"profile_id"`
Status string `json:"status"`
TotalChunks int `json:"total_chunks"`
EmbeddedChunks int `json:"embedded_chunks"`
SkippedChunks int `json:"skipped_chunks"`
FailedChunks int `json:"failed_chunks"`
StartedAt time.Time `json:"started_at"`
UpdatedAt time.Time `json:"updated_at"`
CompletedAt time.Time `json:"completed_at,omitempty"`
ErrorClass string `json:"error_class,omitempty"`
Message string `json:"message,omitempty"`
}
type statusStore interface {
ResolveEmbeddingNamespace(context.Context, cache.EmbeddingNamespaceIdentity) (cache.EmbeddingNamespace, bool, error)
ListChunks(context.Context, cache.ChunkFilter) ([]cache.Chunk, error)
ListChunkEmbeddings(context.Context, cache.ChunkEmbeddingFilter) ([]cache.ChunkEmbedding, error)
ListRAGIndexRuns(context.Context, cache.RAGIndexRunFilter) ([]cache.RAGIndexRun, error)
GetRepoContentState(context.Context, string) (cache.RepoContentState, error)
GetRAGCoverageState(context.Context, string, string) (cache.RAGCoverageState, bool, error)
}
func Status(ctx context.Context, store statusStore, provider EmbeddingProvider, req StatusRequest) (StatusResult, error) {
if store == nil {
return StatusResult{}, fmt.Errorf("rag status: cache store is required")
}
if provider == nil {
return StatusResult{}, fmt.Errorf("rag status: embedding provider is required")
}
if req.RepoID == "" {
return StatusResult{}, fmt.Errorf("rag status: repo id is required")
}
profile := provider.Profile()
result := StatusResult{
RepoID: req.RepoID,
Status: "unknown",
GeneratedAt: time.Now().UTC(),
Provider: ProviderStatus{
ProfileID: firstNonEmpty(req.ProfileID, profile.ProfileID),
ProviderID: profile.ProviderID,
ProviderType: profile.ProviderType,
Endpoint: profile.Endpoint,
Model: profile.Model,
Dimensions: profile.Dimensions,
BatchSize: profile.BatchSize,
},
ActiveJob: req.ActiveJob,
Service: req.Service,
}
req.ChunkPolicyID = firstNonEmpty(req.ChunkPolicyID, DefaultChunkPolicyID)
req.LanguagePolicyID = firstNonEmpty(req.LanguagePolicyID, DefaultLanguagePolicyID)
req.DocumentInstructionID = firstNonEmpty(req.DocumentInstructionID, DefaultDocumentInstructionID)
req.QueryInstructionID = firstNonEmpty(req.QueryInstructionID, DefaultQueryInstructionID)
info, providerErr := provider.ModelInfo(ctx)
if providerErr != nil {
result.Provider.Ready = false
result.Provider.ErrorClass = providerFailureClass(providerErr)
result.Provider.Message = providerErr.Error()
} else {
result.Provider.Ready = true
result.Provider.Model = info.Model
result.Provider.Revision = info.Revision
}
identity, err := provider.NamespaceIdentity(ctx, NamespaceRequest{RepoID: req.RepoID, ChunkPolicyID: req.ChunkPolicyID, LanguagePolicyID: req.LanguagePolicyID, DocumentInstructionID: req.DocumentInstructionID, QueryInstructionID: req.QueryInstructionID})
if err != nil {
result.Status = "provider_not_ready"
result.FailureClass = providerFailureClass(err)
result.Message = err.Error()
return result, nil
}
namespace, ok, err := store.ResolveEmbeddingNamespace(ctx, identity)
if err != nil {
return StatusResult{}, err
}
result.Namespace.Exists = ok
result.Namespace.Current = ok
if ok {
result.Namespace.ID = namespace.ID
}
contentState, err := store.GetRepoContentState(ctx, req.RepoID)
if err != nil {
return StatusResult{}, err
}
result.Coverage.ContentGeneration = contentState.ContentGeneration
if ok {
coverageState, exists, err := store.GetRAGCoverageState(ctx, req.RepoID, namespace.ID)
if err != nil {
return StatusResult{}, err
}
if exists {
result.Coverage.CoveredGeneration = coverageState.CoveredGeneration
result.Coverage.GenerationTracked = true
}
}
chunks, err := store.ListChunks(ctx, cache.ChunkFilter{RepoID: req.RepoID, Policy: req.ChunkPolicyID})
if err != nil {
return StatusResult{}, err
}
result.Coverage.TotalChunks = len(chunks)
currentHashes := make(map[string]string, len(chunks))
for _, chunk := range chunks {
currentHashes[chunk.ID] = chunk.ContentHash
}
if ok {
embeddings, err := store.ListChunkEmbeddings(ctx, cache.ChunkEmbeddingFilter{RepoID: req.RepoID, NamespaceID: namespace.ID})
if err != nil {
return StatusResult{}, err
}
seen := map[string]bool{}
for _, embedding := range embeddings {
hash, exists := currentHashes[embedding.ChunkID]
if !exists {
result.Coverage.StaleChunks++
continue
}
if embedding.ChunkContentHash == hash && embedding.Dimensions == namespace.Dimensions && embedding.DType == namespace.DType {
result.Coverage.EmbeddedChunks++
seen[embedding.ChunkID] = true
continue
}
result.Coverage.StaleChunks++
}
result.Coverage.MissingChunks = result.Coverage.TotalChunks - len(seen)
} else {
result.Coverage.MissingChunks = result.Coverage.TotalChunks
}
runs, err := store.ListRAGIndexRuns(ctx, cache.RAGIndexRunFilter{RepoID: req.RepoID, Limit: 1})
if err != nil {
return StatusResult{}, err
}
if len(runs) > 0 {
run := runStatusFromCache(runs[0])
result.LastRun = &run
result.Coverage.FailedChunks = run.FailedChunks
result.Coverage.SkippedChunks = run.SkippedChunks
}
result.Status = deriveStatus(result)
if providerErr != nil && result.Status == "ready" {
result.Status = "provider_not_ready"
}
return result, nil
}
func runStatusFromCache(run cache.RAGIndexRun) RunStatus {
return RunStatus{ID: run.ID, NamespaceID: run.NamespaceID, ProfileID: run.ProfileID, Status: run.Status, TotalChunks: run.TotalChunks, EmbeddedChunks: run.EmbeddedChunks, SkippedChunks: run.SkippedChunks, FailedChunks: run.FailedChunks, StartedAt: run.StartedAt, UpdatedAt: run.UpdatedAt, CompletedAt: run.CompletedAt, ErrorClass: run.ErrorClass, Message: run.Message}
}
func deriveStatus(result StatusResult) string {
if result.ActiveJob != nil && (result.ActiveJob.Status == RAGIndexStatusQueued || result.ActiveJob.Status == RAGIndexStatusRunning || result.ActiveJob.Status == "queued" || result.ActiveJob.Status == "running") {
return "indexing"
}
if !result.Provider.Ready {
return "provider_not_ready"
}
if !result.Namespace.Exists {
if result.Coverage.TotalChunks == 0 {
return "empty"
}
return "no_namespace"
}
if result.Coverage.TotalChunks == 0 {
return "empty"
}
if result.LastRun != nil && result.LastRun.Status == RAGIndexStatusFailed {
return "failed"
}
if result.Coverage.MissingChunks > 0 || result.Coverage.StaleChunks > 0 || result.Coverage.FailedChunks > 0 || (result.Coverage.GenerationTracked && result.Coverage.CoveredGeneration < result.Coverage.ContentGeneration) {
return "partial"
}
return "ready"
}