Copilot commented on code in PR #1421:
URL: https://github.com/apache/dubbo-admin/pull/1421#discussion_r2901808009
##########
ai/cmd/index.go:
##########
@@ -0,0 +1,177 @@
+package main
+
+import (
+ "context"
+ compRag "dubbo-admin-ai/component/rag"
+ appconfig "dubbo-admin-ai/config"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/firebase/genkit/go/core/api"
+ "github.com/firebase/genkit/go/genkit"
+ "github.com/firebase/genkit/go/plugins/compat_oai"
+ "github.com/firebase/genkit/go/plugins/googlegenai"
+ "github.com/firebase/genkit/go/plugins/pinecone"
+ "github.com/openai/openai-go/option"
+)
+
+type IndexCommand struct {
+ Directory string
+ ConfigPath string
+}
+
+func main() {
+ cmd := parseFlags()
+
+ if err := validateCommand(cmd); err != nil {
+ log.Fatalf("Validation error: %v", err)
+ }
+
+ if err := executeIndexing(cmd); err != nil {
+ log.Fatalf("Indexing failed: %v", err)
+ }
+
+ fmt.Println("Indexing completed successfully")
+}
+
+func parseFlags() *IndexCommand {
+ cmd := &IndexCommand{}
+
+ flag.StringVar(&cmd.Directory, "dir",
"/Users/liwener/programming/ospp/dubbo-admin/ai/reference/k8s_docs/concepts",
"Directory to index (required)")
Review Comment:
The `-dir` flag default is a developer-specific absolute path
("/Users/liwener/..."), which will break for other environments. Prefer an
empty default (and require the flag) or a repo-relative path (e.g.,
"./reference/..."), and document it in the usage string.
```suggestion
flag.StringVar(&cmd.Directory, "dir", "", "Directory to index
(required, no default)")
```
##########
ai/component/rag/indexer.go:
##########
@@ -0,0 +1,203 @@
+package rag
+
+import (
+ "context"
+
+ "dubbo-admin-ai/utils"
+ "fmt"
+ "sync"
+
+ "github.com/cloudwego/eino/components/indexer"
+ "github.com/cloudwego/eino/schema"
+ "github.com/firebase/genkit/go/ai"
+ "github.com/firebase/genkit/go/core"
+ "github.com/firebase/genkit/go/genkit"
+ "github.com/firebase/genkit/go/plugins/localvec"
+ "github.com/firebase/genkit/go/plugins/pinecone"
+)
+
+// --- Indexer ---
+type PineconeIndexer struct {
+ g *genkit.Genkit
+ embedder string
+ target string
+ batchSz int
+ mu sync.Mutex
+ docstore map[string]*pinecone.Docstore // keyed by target index
+}
+
+func newPineconeIndexer(g *genkit.Genkit, embedderModel string, targetIndex
string, batchSize int) *PineconeIndexer {
+ return &PineconeIndexer{
+ g: g,
+ embedder: embedderModel,
+ target: targetIndex,
+ batchSz: batchSize,
+ }
+}
+
+func (idx *PineconeIndexer) Store(ctx context.Context, docs
[]*schema.Document, opts ...indexer.Option) ([]string, error) {
+ // Handle options
+ implOpts := indexer.GetImplSpecificOptions(&CommonIndexerOptions{},
opts...)
+ namespace := implOpts.Namespace
+ effectiveTarget := idx.target
+ if implOpts.TargetIndex != nil && *implOpts.TargetIndex != "" {
+ effectiveTarget = *implOpts.TargetIndex
+ }
+
+ // TODO(indexer, 2026-02-24): Validate namespace if needed for
multi-tenancy support
+ // Initialize indexer docstore for this target if not already done
+ idx.mu.Lock()
+ if idx.docstore == nil {
+ idx.docstore = make(map[string]*pinecone.Docstore)
+ }
+ docstore := idx.docstore[effectiveTarget]
+ idx.mu.Unlock()
+ if docstore == nil {
+ embedder := genkit.LookupEmbedder(idx.g, idx.embedder)
+ if embedder == nil {
+ return nil, fmt.Errorf("failed to find embedder %s",
idx.embedder)
+ }
+
+ // Configure Pinecone connection
+ pineconeConfig := pinecone.Config{
+ IndexID: effectiveTarget,
+ Embedder: embedder,
+ }
+
+ newDocstore, _, err := pinecone.DefineRetriever(ctx, idx.g,
+ pineconeConfig,
+ &ai.RetrieverOptions{
+ Label: effectiveTarget,
+ ConfigSchema:
core.InferSchemaMap(pinecone.PineconeRetrieverOptions{}),
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to setup retriever for
indexer: %w", err)
+ }
+
+ idx.mu.Lock()
+ if idx.docstore == nil {
+ idx.docstore = make(map[string]*pinecone.Docstore)
+ }
+ if idx.docstore[effectiveTarget] == nil {
+ idx.docstore[effectiveTarget] = newDocstore
+ }
+ docstore = idx.docstore[effectiveTarget]
+ idx.mu.Unlock()
+ }
+
+ // Convert to Genkit documents
+ genkitDocs := utils.ToGenkitDocuments(docs)
+
+ // Index in batches
+ batchSize := idx.batchSz
+ if implOpts.BatchSize != nil && *implOpts.BatchSize > 0 {
+ batchSize = *implOpts.BatchSize
+ }
+ if batchSize <= 0 {
+ return nil, fmt.Errorf("batch size must be positive")
+ }
+ for i := 0; i < len(genkitDocs); i += batchSize {
+ end := min(i+batchSize, len(genkitDocs))
+ batch := genkitDocs[i:end]
+ if err := pinecone.Index(ctx, batch, docstore, namespace); err
!= nil {
+ return nil, fmt.Errorf("failed to index documents batch
%d-%d: %w", i+1, end, err)
+ }
+ }
+
+ return nil, nil
+}
Review Comment:
PineconeIndexer.Store returns `(nil, nil)` after successful indexing. The
indexer contract expects IDs for the stored documents (DevIndexer returns
them), and returning nil can break callers that rely on IDs or interpret nil as
“nothing stored”. Consider returning the input document IDs (or IDs returned by
the backend if available).
##########
ai/runtime/runtime.go:
##########
@@ -0,0 +1,237 @@
+package runtime
+
+import (
+ "context"
+ "dubbo-admin-ai/config"
+ "fmt"
+ "log/slog"
+ "sync"
+
+ "github.com/firebase/genkit/go/genkit"
+ "gopkg.in/yaml.v3"
+)
+
+// Component defines the interface for all components
+type Component interface {
+ Name() string
+ Validate() error
+ Init(*Runtime) error
+ Start() error
+ Stop() error
+}
+
+// ComponentFactory is the function type for creating components
+type ComponentFactory func(config *yaml.Node) (Component, error)
+
+var (
+ gloRuntime *Runtime = nil
+)
+
+func NewRuntime() *Runtime {
+ return &Runtime{
+ factories: make(map[string]ComponentFactory),
+ factoryOrder: make([]string, 0),
+ genkitOptions: make([]genkit.GenkitOption, 0),
+ }
+}
+
+func Bootstrap(configFile string, registerFn func(rt *Runtime)) (*Runtime,
error) {
+ gloRuntime = NewRuntime()
+
+ // Register component factories
+ if registerFn != nil {
+ registerFn(gloRuntime)
+ }
+
+ // Create config loader and load all configurations
+ loader := config.NewLoader(configFile)
+ loadedCfg, err := loader.Load()
+ if err != nil {
+ return nil, fmt.Errorf("failed to load config: %w", err)
+ }
+
+ // Create component instances
+ instances, err := gloRuntime.createComponents(loadedCfg)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create components: %w", err)
+ }
+
+ // Initialize components in dependency order, which is the order of
factory registration.
+ for _, comp := range instances {
+ if err := comp.Validate(); err != nil {
+ return nil, fmt.Errorf("failed to validate %s: %w",
comp.Name(), err)
+ }
+
+ if err := comp.Init(gloRuntime); err != nil {
+ return nil, fmt.Errorf("failed to init %s: %w",
comp.Name(), err)
+ }
+ gloRuntime.Components.Store(comp.Name(), comp)
+ }
Review Comment:
Components are stored in the runtime by `comp.Name()`. Since many components
return a fixed name (e.g., "agent"), this will overwrite entries when
`components.<name>` in config.yaml is an array (Loader generates names like
`agent-0`, `agent-1`). To support multiple instances, store components under
the config key (name from LoadedConfig.Components) and/or allow components to
be instantiated with a unique name.
##########
ai/test/models.md:
##########
@@ -0,0 +1,146 @@
+# 模型可用性测试指南
+
+本指南说明如何测试 `config/models.yaml` 中配置的所有模型是否可用。
+
+## 测试方法
+
+### 方法1:使用现有测试(推荐)
+
+AI模块已经有一个简单的文本生成测试,可以快速验证默认模型:
+
+```bash
+cd /Users/liwener/programming/ospp/dubbo-admin/ai
+
+# 设置API密钥
+export DASHSCOPE_API_KEY="your_qwen_api_key"
+
+# 运行测试
+go test -v ./test/ -run TestTextGeneration
+```
Review Comment:
The guide uses absolute paths (e.g., `cd /Users/.../ai`) which are not
portable. Prefer repo-relative commands (e.g., `cd ai`) so the documentation
works for all contributors.
##########
ai/runtime/runtime.go:
##########
@@ -0,0 +1,237 @@
+package runtime
+
+import (
+ "context"
+ "dubbo-admin-ai/config"
+ "fmt"
+ "log/slog"
+ "sync"
+
+ "github.com/firebase/genkit/go/genkit"
+ "gopkg.in/yaml.v3"
+)
+
+// Component defines the interface for all components
+type Component interface {
+ Name() string
+ Validate() error
+ Init(*Runtime) error
+ Start() error
+ Stop() error
+}
+
+// ComponentFactory is the function type for creating components
+type ComponentFactory func(config *yaml.Node) (Component, error)
+
+var (
+ gloRuntime *Runtime = nil
+)
+
+func NewRuntime() *Runtime {
+ return &Runtime{
+ factories: make(map[string]ComponentFactory),
+ factoryOrder: make([]string, 0),
+ genkitOptions: make([]genkit.GenkitOption, 0),
+ }
+}
+
+func Bootstrap(configFile string, registerFn func(rt *Runtime)) (*Runtime,
error) {
+ gloRuntime = NewRuntime()
+
+ // Register component factories
+ if registerFn != nil {
+ registerFn(gloRuntime)
+ }
+
+ // Create config loader and load all configurations
+ loader := config.NewLoader(configFile)
+ loadedCfg, err := loader.Load()
+ if err != nil {
+ return nil, fmt.Errorf("failed to load config: %w", err)
+ }
+
+ // Create component instances
+ instances, err := gloRuntime.createComponents(loadedCfg)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create components: %w", err)
+ }
+
+ // Initialize components in dependency order, which is the order of
factory registration.
+ for _, comp := range instances {
+ if err := comp.Validate(); err != nil {
+ return nil, fmt.Errorf("failed to validate %s: %w",
comp.Name(), err)
+ }
+
+ if err := comp.Init(gloRuntime); err != nil {
+ return nil, fmt.Errorf("failed to init %s: %w",
comp.Name(), err)
+ }
+ gloRuntime.Components.Store(comp.Name(), comp)
+ }
+
+ // Start all loaded components
+ gloRuntime.Components.Range(func(key, value any) bool {
+ comp := value.(Component)
+ if err := comp.Start(); err != nil {
+ return false
+ }
+ return true
+ })
Review Comment:
Bootstrap starts components via sync.Map.Range, which has non-deterministic
iteration order and discards the Start() error (the callback returns false but
the error is not surfaced). This can leave the runtime partially started while
main logs success. Consider starting components in the same deterministic order
as initialization (e.g., iterate the `instances` slice) and return the first
start error from Bootstrap (and/or track started components for rollback).
##########
ai/cmd/index.go:
##########
@@ -0,0 +1,177 @@
+package main
+
+import (
+ "context"
+ compRag "dubbo-admin-ai/component/rag"
+ appconfig "dubbo-admin-ai/config"
+ "flag"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/firebase/genkit/go/core/api"
+ "github.com/firebase/genkit/go/genkit"
+ "github.com/firebase/genkit/go/plugins/compat_oai"
+ "github.com/firebase/genkit/go/plugins/googlegenai"
+ "github.com/firebase/genkit/go/plugins/pinecone"
+ "github.com/openai/openai-go/option"
+)
+
+type IndexCommand struct {
+ Directory string
+ ConfigPath string
+}
+
+func main() {
+ cmd := parseFlags()
+
+ if err := validateCommand(cmd); err != nil {
+ log.Fatalf("Validation error: %v", err)
+ }
+
+ if err := executeIndexing(cmd); err != nil {
+ log.Fatalf("Indexing failed: %v", err)
+ }
+
+ fmt.Println("Indexing completed successfully")
+}
+
+func parseFlags() *IndexCommand {
+ cmd := &IndexCommand{}
+
+ flag.StringVar(&cmd.Directory, "dir",
"/Users/liwener/programming/ospp/dubbo-admin/ai/reference/k8s_docs/concepts",
"Directory to index (required)")
+ flag.StringVar(&cmd.ConfigPath, "config", "component/rag/rag.yaml",
"Configuration file path")
+
+ flag.Parse()
+ return cmd
+}
+
+func validateCommand(cmd *IndexCommand) error {
+ if cmd.Directory == "" {
+ return fmt.Errorf("directory parameter is required")
+ }
+
+ // Check if directory exists
+ info, err := os.Stat(cmd.Directory)
+ if err != nil {
+ return fmt.Errorf("failed to access directory %s: %w",
cmd.Directory, err)
+ }
+
+ if !info.IsDir() {
+ return fmt.Errorf("%s is not a directory", cmd.Directory)
+ }
+
+ // Check if config file exists
+ if _, err := os.Stat(cmd.ConfigPath); err != nil {
+ return fmt.Errorf("failed to access config file %s: %w",
cmd.ConfigPath, err)
+ }
+
+ return nil
+}
+
+func executeIndexing(cmd *IndexCommand) error {
+ ctx := context.Background()
+
+ // Load configuration from YAML file
+ cfg, err := loadRAGConfig(cmd.ConfigPath)
+ if err != nil {
+ return fmt.Errorf("failed to load config: %w", err)
+ }
+ if err := cfg.Validate(); err != nil {
+ return fmt.Errorf("invalid rag config: %w", err)
+ }
+
+ // Initialize Genkit registry independently
+ plugins := []api.Plugin{}
+ // Initialize plugins from environment variables
+ if key := os.Getenv("DASHSCOPE_API_KEY"); key != "" {
+ plugins = append(plugins, &compat_oai.OpenAICompatible{
+ Provider: "dashscope",
+ Opts: []option.RequestOption{
+ option.WithAPIKey(key),
+
option.WithBaseURL("https://dashscope.aliyuncs.com/compatible-mode/v1"),
+ },
+ })
+ }
+ if key := os.Getenv("GEMINI_API_KEY"); key != "" {
+ plugins = append(plugins, &googlegenai.GoogleAI{APIKey: key})
+ }
+ if key := os.Getenv("SILICONFLOW_API_KEY"); key != "" {
+ plugins = append(plugins, &compat_oai.OpenAICompatible{
+ Provider: "siliconflow",
+ Opts: []option.RequestOption{
+ option.WithAPIKey(key),
+
option.WithBaseURL("https://api.siliconflow.cn/v1"),
+ },
+ })
+ }
+ if key := os.Getenv("PINECONE_API_KEY"); key != "" {
+ plugins = append(plugins, &pinecone.Pinecone{APIKey: key})
+ }
+
+ g := genkit.Init(ctx, genkit.WithPlugins(plugins...))
+
+ // Build-time target index selection: default to directory name for
this CLI
+ targetIndex := getNamespace("", cmd.Directory)
+
+ sys, err := compRag.BuildRAGFromSpec(ctx, g, cfg)
+ if err != nil {
+ return fmt.Errorf("failed to build RAG system: %w", err)
+ }
+
+ // Load documents via the built loader
+ docs, err := compRag.LoadDirectory(ctx, sys.Loader, cmd.Directory)
+ if err != nil {
+ return fmt.Errorf("failed to load directory: %w", err)
+ }
+
+ fmt.Printf("Loaded %d documents from %s\n", len(docs), cmd.Directory)
+
+ if len(docs) == 0 {
+ fmt.Printf("No supported documents found in directory: %s\n",
cmd.Directory)
+ return nil
+ }
+
+ // Split documents (semantic/header or recursive based on config)
+ splitDocs, err := sys.Split(ctx, docs)
+ if err != nil {
+ return fmt.Errorf("failed to split documents: %w", err)
+ }
+
+ // Index documents (namespace is a per-call runtime parameter)
+ namespace := targetIndex
+ if _, err := sys.Index(ctx, namespace, splitDocs,
compRag.WithIndexerTargetIndex(targetIndex)); err != nil {
+ return fmt.Errorf("failed to index documents: %w", err)
+ }
+
+ return nil
+}
+
+// getNamespace generates a namespace from the provided parameter or directory
name
+func getNamespace(namespace, directory string) string {
+ if namespace != "" {
+ return namespace
+ }
+ return filepath.Base(strings.TrimSpace(directory))
+}
+
+// loadRAGConfig loads RAG configuration from a YAML file
+func loadRAGConfig(configPath string) (*compRag.RAGSpec, error) {
+ loader := appconfig.NewLoader("config.yaml")
+ componentCfg, err := loader.LoadComponent(configPath)
+ if err != nil {
Review Comment:
loadRAGConfig initializes the Loader with `config.NewLoader("config.yaml")`,
so schema/config resolution becomes dependent on the current working directory
rather than the provided `configPath`. This makes the CLI fragile when run
outside the ai/ directory. Consider constructing the Loader with a config file
path rooted at `filepath.Dir(configPath)` (or using absolute paths) so relative
component paths and default schema path resolve reliably.
```suggestion
// Resolve the provided config path to an absolute path so that loading
// does not depend on the current working directory.
absConfigPath, err := filepath.Abs(configPath)
if err != nil {
return nil, fmt.Errorf("failed to resolve absolute path for
config %s: %w", configPath, err)
}
// Construct the loader's config file path relative to the config's
directory.
baseDir := filepath.Dir(absConfigPath)
loaderConfigPath := filepath.Join(baseDir, "config.yaml")
loader := appconfig.NewLoader(loaderConfigPath)
componentCfg, err := loader.LoadComponent(absConfigPath)
if err != nil {
```
##########
ai/schema/json/README.md:
##########
@@ -0,0 +1,19 @@
+# JSON Schema Index
+
+This directory contains JSON Schema definitions for YAML configs.
+
+- `main.schema.json`: root
[`config.yaml`](/Users/liwener/.codex/worktrees/acbb/dubbo-admin/ai/config.yaml)
+- `logger.schema.json`:
[`component/logger/logger.yaml`](/Users/liwener/.codex/worktrees/acbb/dubbo-admin/ai/component/logger/logger.yaml)
+- `memory.schema.json`:
[`component/memory/memory.yaml`](/Users/liwener/.codex/worktrees/acbb/dubbo-admin/ai/component/memory/memory.yaml)
+- `models.schema.json`:
[`component/models/models.yaml`](/Users/liwener/.codex/worktrees/acbb/dubbo-admin/ai/component/models/models.yaml)
+- `tools.schema.json`:
[`component/tools/tools.yaml`](/Users/liwener/.codex/worktrees/acbb/dubbo-admin/ai/component/tools/tools.yaml)
+- `server.schema.json`:
[`component/server/server.yaml`](/Users/liwener/.codex/worktrees/acbb/dubbo-admin/ai/component/server/server.yaml)
+- `rag.schema.json`:
[`component/rag/rag.yaml`](/Users/liwener/.codex/worktrees/acbb/dubbo-admin/ai/component/rag/rag.yaml)
+- `agent.schema.json`:
[`component/agent/agent.yaml`](/Users/liwener/.codex/worktrees/acbb/dubbo-admin/ai/component/agent/agent.yaml)
Review Comment:
This README links to schema/config files using absolute local filesystem
paths under `/Users/...`, which won’t work for other contributors or in GitHub
UI. Use repository-relative links (e.g., `../../config.yaml` or
`/ai/config.yaml`) instead.
##########
ai/config/jsonschema.go:
##########
@@ -0,0 +1,175 @@
+package config
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/xeipuuv/gojsonschema"
+)
+
+// schemaEngine manages the loading, caching, and compilation of JSON schemas.
+type schemaEngine struct {
+ baseDir string
+ mu sync.RWMutex
+ cache map[string]*gojsonschema.Schema
+ rootObjs map[string]map[string]any
+}
+
+// NewSchemaEngine creates a new schemaEngine with the specified base
directory.
+func NewSchemaEngine(baseDir string) *schemaEngine {
+ return &schemaEngine{
+ baseDir: baseDir,
+ cache: make(map[string]*gojsonschema.Schema),
+ rootObjs: make(map[string]map[string]any),
+ }
+}
+
+// ApplyDefaultsAndValidate applies default values and validates.
+// This method modifies doc in-place by applying defaults, then validates it.
+//
+// Parameters:
+// - doc: The configuration document (WILL BE MODIFIED)
+// - schemaFile: The schema filename
+//
+// Returns:
+// - The modified doc (same map as input)
+// - Error if validation fails
+func (e *schemaEngine) ApplyDefaultsAndValidate(doc map[string]any, schemaFile
string) (map[string]any, error) {
+ compiled, rootObj, err := e.loadSchema(schemaFile)
+ if err != nil {
+ return nil, err
+ }
+
+ // Apply defaults in-place (modifies doc)
+ applyDefaults(rootObj, rootObj, doc)
+
+ // Validate the result
+ if err := validateJSONSchema(compiled, doc); err != nil {
+ return nil, err
+ }
+
+ return doc, nil
+}
+
+func (e *schemaEngine) loadSchema(fileName string) (*gojsonschema.Schema,
map[string]any, error) {
+ e.mu.RLock()
+ compiled, hasCached := e.cache[fileName]
+ rootObj, hasRoot := e.rootObjs[fileName]
+ e.mu.RUnlock()
+
+ if hasCached && hasRoot {
+ return compiled, rootObj, nil
+ }
+
+ fullPath := filepath.Join(e.baseDir, fileName)
+ raw, err := os.ReadFile(fullPath)
+ if err != nil {
+ return nil, nil, fmt.Errorf("structural error: failed to read
schema file %s: %w", fileName, err)
+ }
+
+ var schemaObj map[string]any
+ if err := json.Unmarshal(raw, &schemaObj); err != nil {
+ return nil, nil, fmt.Errorf("structural error: failed to parse
schema file %s: %w", fileName, err)
+ }
+
+ compiled, err = gojsonschema.NewSchema(gojsonschema.NewBytesLoader(raw))
+ if err != nil {
+ return nil, nil, fmt.Errorf("structural error: failed to
compile schema file %s: %w", fileName, err)
+ }
+
+ e.mu.Lock()
+ e.cache[fileName] = compiled
+ e.rootObjs[fileName] = schemaObj
+ e.mu.Unlock()
+ return compiled, schemaObj, nil
+}
+
+func validateJSONSchema(compiled *gojsonschema.Schema, doc any) error {
+ docRaw, err := json.Marshal(doc)
+ if err != nil {
+ return fmt.Errorf("failed to marshal config for schema
validation: %w", err)
+ }
+
+ result, err := compiled.Validate(gojsonschema.NewBytesLoader(docRaw))
+ if err != nil {
+ return fmt.Errorf("failed to validate schema: %w", err)
+ }
+ if result.Valid() {
+ return nil
+ }
+
+ errMsgs := make([]string, 0, len(result.Errors()))
+ for _, e := range result.Errors() {
+ field := e.Field()
+ if field == "(root)" || field == "" {
+ field = "root"
+ }
+ errMsgs = append(errMsgs, fmt.Sprintf("%s: %s", field,
e.Description()))
+ }
+ return fmt.Errorf("structural error: %s", strings.Join(errMsgs, "; "))
+}
+
+// applyDefaults recursively applies default values from schema to value
+// Modifies value in-place
+func applyDefaults(root map[string]any, schema map[string]any, value any) {
+ resolved := resolveSchemaRef(root, schema)
+
+ switch v := value.(type) {
+ case map[string]any:
+ props, _ := resolved["properties"].(map[string]any)
+ for key, propVal := range props {
+ propSchema, ok := propVal.(map[string]any)
+ if !ok {
+ continue
+ }
+ propSchema = resolveSchemaRef(root, propSchema)
+
+ // Apply default value if property is missing
+ if _, exists := v[key]; !exists {
+ if defVal, hasDefault := propSchema["default"];
hasDefault {
+ v[key] = defVal
+ }
+ }
+
+ // Recursively apply defaults to nested properties
+ if child, exists := v[key]; exists {
+ applyDefaults(root, propSchema, child)
+ }
+ }
+
+ case []any:
+ if items, ok := resolved["items"].(map[string]any); ok {
+ items = resolveSchemaRef(root, items)
+ for i := range v {
+ applyDefaults(root, items, v[i])
+ }
+ }
+ }
Review Comment:
`applyDefaults` only walks `properties`/`items` (and `$ref`) but does not
handle schemas that use `oneOf`/`anyOf`/`allOf` (used by rag.schema.json for
splitter.spec). This means schema defaults inside those branches won't be
injected, and later semantic validation can fail even though the schema defines
defaults. Consider extending default application to descend into composition
keywords (e.g., choose a branch based on `type` discriminator when available,
or apply defaults across all branches where safe).
##########
ai/component/rag/retriever.go:
##########
@@ -0,0 +1,237 @@
+package rag
+
+import (
+ "context"
+
+ "dubbo-admin-ai/utils"
+ "fmt"
+ "sync"
+
+ "github.com/cloudwego/eino/components/retriever"
+ "github.com/cloudwego/eino/schema"
+ "github.com/firebase/genkit/go/ai"
+ "github.com/firebase/genkit/go/core"
+ "github.com/firebase/genkit/go/genkit"
+ "github.com/firebase/genkit/go/plugins/localvec"
+ "github.com/firebase/genkit/go/plugins/pinecone"
+)
+
+// --- Retriever ---
+type PineconeRetriever struct {
+ g *genkit.Genkit
+ embedder string
+ target string
+ defaultK int
+ retriever map[string]ai.Retriever // keyed by target index
+}
+
+func newPineconeRetriever(g *genkit.Genkit, embedderModel string, targetIndex
string, topK int) *PineconeRetriever {
+ return &PineconeRetriever{
+ g: g,
+ embedder: embedderModel,
+ target: targetIndex,
+ defaultK: topK,
+ }
+}
+
+func (r *PineconeRetriever) getRetriever(ctx context.Context, targetIndex
string) (ai.Retriever, error) {
+ if targetIndex == "" {
+ targetIndex = "default"
+ }
+
+ if r.retriever == nil {
+ r.retriever = make(map[string]ai.Retriever)
+ }
+ ret := r.retriever[targetIndex]
+ if ret != nil {
Review Comment:
PineconeRetriever caches retrievers in a plain map without synchronization.
`getRetriever` both reads and writes `r.retriever`, so concurrent Retrieve
calls can race and panic. DevRetriever uses a mutex—PineconeRetriever should
similarly guard map access (or use sync.Map).
##########
ai/component/server/server.yaml:
##########
@@ -0,0 +1,8 @@
+type: server
+spec:
+ port: 8880 # Server port
+ host: "localhost" # Server host
+ debug: false # Debug mode
Review Comment:
server.yaml sets `host: "localhost"`, which binds only to loopback and can
break container/K8s deployments where the service must listen on all
interfaces. Consider using `0.0.0.0` (matching the schema default) unless
there’s a specific reason to restrict binding.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]