Add initial implementation of MCP server and pgvecter API integration
- Introduced the MCP server with core functionality for handling document upserts and searches using the pgvecter API. - Added configuration management for API keys and base URL. - Implemented RPC request handling for various tools, including dev log and trade event management. - Expanded the README with endpoint details and usage examples for the new MCP server features. - Created documentation for development notes and API specifications in `docs/mcp_dev_memo.md`. - Added necessary types and client implementations for interacting with the pgvecter API.
This commit is contained in:
13
README.md
13
README.md
@@ -394,6 +394,19 @@ curl -s http://127.0.0.1:8092/health
|
||||
1. `X-API-KEY` を必ず送る
|
||||
2. `permissions.users` はサーバ側で強制注入される前提
|
||||
|
||||
### エンドポイント一覧(他サービス向け)
|
||||
|
||||
- `GET /health`: ヘルスチェック(認証不要)
|
||||
- `POST /db/query`: SELECT-only の読み取りクエリ(`X-API-KEY` 必須)
|
||||
- `POST /kb/upsert`: 1ドキュメントを upsert(`X-API-KEY` 必須、`id` は UUID)
|
||||
- `POST /kb/search`: ベクトル検索(`X-API-KEY` 必須)
|
||||
- `GET /collections`: コレクション一覧(`X-API-KEY` 必須)
|
||||
- `GET /admin/collections`: 管理者向けコレクション一覧(`X-ADMIN-API-KEY` 必須)
|
||||
- `GET /admin/api-keys`: 管理APIキー一覧(`X-ADMIN-API-KEY` 必須)
|
||||
- `POST /admin/api-keys`: 管理APIキー作成(`X-ADMIN-API-KEY` 必須)
|
||||
- `PATCH /admin/api-keys/{id}`: 管理APIキー更新(`X-ADMIN-API-KEY` 必須)
|
||||
- `POST /admin/api-keys/{id}/revoke`: 管理APIキー失効(`X-ADMIN-API-KEY` 必須)
|
||||
|
||||
### 例: /kb/search
|
||||
|
||||
```bash
|
||||
|
||||
25
cmd/mcp-server/config.go
Normal file
25
cmd/mcp-server/config.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
}
|
||||
|
||||
func LoadConfig() (Config, error) {
|
||||
cfg := Config{
|
||||
BaseURL: os.Getenv("PGVECTER_BASE_URL"),
|
||||
APIKey: os.Getenv("PGVECTER_API_KEY"),
|
||||
}
|
||||
if cfg.BaseURL == "" {
|
||||
return cfg, errors.New("PGVECTER_BASE_URL is required")
|
||||
}
|
||||
if cfg.APIKey == "" {
|
||||
return cfg, errors.New("PGVECTER_API_KEY is required")
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
23
cmd/mcp-server/main.go
Normal file
23
cmd/mcp-server/main.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"pgvecterapi/internal/mcp"
|
||||
"pgvecterapi/internal/pgvecter"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := LoadConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
client := pgvecter.NewClient(cfg.BaseURL, cfg.APIKey)
|
||||
tools := mcp.NewToolSet(client)
|
||||
|
||||
if err := runMCPServer(context.Background(), tools); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
408
cmd/mcp-server/mcp.go
Normal file
408
cmd/mcp-server/mcp.go
Normal file
@@ -0,0 +1,408 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"pgvecterapi/internal/mcp"
|
||||
)
|
||||
|
||||
const protocolVersion = "2025-03-26"
|
||||
|
||||
type rpcRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.RawMessage `json:"id,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type rpcResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.RawMessage `json:"id"`
|
||||
Result any `json:"result,omitempty"`
|
||||
Error *rpcError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type rpcError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type initializeParams struct {
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
Capabilities map[string]any `json:"capabilities,omitempty"`
|
||||
ClientInfo map[string]any `json:"clientInfo,omitempty"`
|
||||
}
|
||||
|
||||
type initializeResult struct {
|
||||
ProtocolVersion string `json:"protocolVersion"`
|
||||
Capabilities map[string]any `json:"capabilities"`
|
||||
ServerInfo map[string]any `json:"serverInfo"`
|
||||
}
|
||||
|
||||
type toolsListResult struct {
|
||||
Tools []toolDef `json:"tools"`
|
||||
NextCursor string `json:"nextCursor,omitempty"`
|
||||
}
|
||||
|
||||
type toolDef struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description"`
|
||||
InputSchema map[string]any `json:"inputSchema"`
|
||||
}
|
||||
|
||||
type toolsCallParams struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments"`
|
||||
}
|
||||
|
||||
type toolsCallResult struct {
|
||||
Content []contentItem `json:"content"`
|
||||
IsError bool `json:"isError"`
|
||||
}
|
||||
|
||||
type contentItem struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func runMCPServer(ctx context.Context, tools *mcp.ToolSet) error {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
buf := make([]byte, 0, 1024*1024)
|
||||
scanner.Buffer(buf, 10*1024*1024)
|
||||
|
||||
writer := bufio.NewWriter(os.Stdout)
|
||||
defer writer.Flush()
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var req rpcRequest
|
||||
if err := json.Unmarshal([]byte(line), &req); err != nil {
|
||||
_ = writeRPCError(writer, nil, -32700, "parse error", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if req.Method == "" {
|
||||
_ = writeRPCError(writer, req.ID, -32600, "invalid request", "missing method")
|
||||
continue
|
||||
}
|
||||
|
||||
// Notifications have no id
|
||||
if len(req.ID) == 0 {
|
||||
if req.Method == "initialized" {
|
||||
continue
|
||||
}
|
||||
// Ignore unknown notifications
|
||||
continue
|
||||
}
|
||||
|
||||
switch req.Method {
|
||||
case "initialize":
|
||||
res := initializeResult{
|
||||
ProtocolVersion: protocolVersion,
|
||||
Capabilities: map[string]any{
|
||||
"tools": map[string]any{"listChanged": false},
|
||||
},
|
||||
ServerInfo: map[string]any{
|
||||
"name": "pgvecter-mcp",
|
||||
"version": "0.1.0",
|
||||
},
|
||||
}
|
||||
_ = writeRPCResult(writer, req.ID, res)
|
||||
case "tools/list":
|
||||
res := toolsListResult{Tools: toolList()}
|
||||
_ = writeRPCResult(writer, req.ID, res)
|
||||
case "tools/call":
|
||||
out, err := handleToolCall(ctx, tools, req.Params)
|
||||
if err != nil {
|
||||
_ = writeRPCResult(writer, req.ID, toolsCallResult{
|
||||
Content: []contentItem{{Type: "text", Text: err.Error()}},
|
||||
IsError: true,
|
||||
})
|
||||
continue
|
||||
}
|
||||
_ = writeRPCResult(writer, req.ID, out)
|
||||
default:
|
||||
_ = writeRPCError(writer, req.ID, -32601, "method not found", req.Method)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleToolCall(ctx context.Context, tools *mcp.ToolSet, raw json.RawMessage) (toolsCallResult, error) {
|
||||
var params toolsCallParams
|
||||
if err := json.Unmarshal(raw, ¶ms); err != nil {
|
||||
return toolsCallResult{}, fmt.Errorf("invalid params: %w", err)
|
||||
}
|
||||
if params.Name == "" {
|
||||
return toolsCallResult{}, errors.New("tool name is required")
|
||||
}
|
||||
|
||||
marshal := func(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
unmarshal := func(b []byte, v any) error {
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
switch params.Name {
|
||||
case "dev_log_upsert":
|
||||
var in mcp.DevLogUpsertInput
|
||||
b, _ := marshal(params.Arguments)
|
||||
if err := unmarshal(b, &in); err != nil {
|
||||
return toolsCallResult{}, err
|
||||
}
|
||||
res, err := tools.DevLogUpsert(ctx, in)
|
||||
return formatResult(res, err)
|
||||
case "dev_log_search":
|
||||
var in mcp.DevLogSearchInput
|
||||
b, _ := marshal(params.Arguments)
|
||||
if err := unmarshal(b, &in); err != nil {
|
||||
return toolsCallResult{}, err
|
||||
}
|
||||
res, err := tools.DevLogSearch(ctx, in)
|
||||
return formatResult(res, err)
|
||||
case "trade_event_upsert":
|
||||
var in mcp.TradeEventUpsertInput
|
||||
b, _ := marshal(params.Arguments)
|
||||
if err := unmarshal(b, &in); err != nil {
|
||||
return toolsCallResult{}, err
|
||||
}
|
||||
res, err := tools.TradeEventUpsert(ctx, in)
|
||||
return formatResult(res, err)
|
||||
case "trade_event_search":
|
||||
var in mcp.TradeEventSearchInput
|
||||
b, _ := marshal(params.Arguments)
|
||||
if err := unmarshal(b, &in); err != nil {
|
||||
return toolsCallResult{}, err
|
||||
}
|
||||
res, err := tools.TradeEventSearch(ctx, in)
|
||||
return formatResult(res, err)
|
||||
case "doc_upsert":
|
||||
var in mcp.DocUpsertInput
|
||||
b, _ := marshal(params.Arguments)
|
||||
if err := unmarshal(b, &in); err != nil {
|
||||
return toolsCallResult{}, err
|
||||
}
|
||||
res, err := tools.DocUpsert(ctx, in)
|
||||
return formatResult(res, err)
|
||||
case "doc_search":
|
||||
var in mcp.DocSearchInput
|
||||
b, _ := marshal(params.Arguments)
|
||||
if err := unmarshal(b, &in); err != nil {
|
||||
return toolsCallResult{}, err
|
||||
}
|
||||
res, err := tools.DocSearch(ctx, in)
|
||||
return formatResult(res, err)
|
||||
case "security_upsert":
|
||||
var in mcp.SecurityUpsertInput
|
||||
b, _ := marshal(params.Arguments)
|
||||
if err := unmarshal(b, &in); err != nil {
|
||||
return toolsCallResult{}, err
|
||||
}
|
||||
res, err := tools.SecurityUpsert(ctx, in)
|
||||
return formatResult(res, err)
|
||||
case "security_search":
|
||||
var in mcp.SecuritySearchInput
|
||||
b, _ := marshal(params.Arguments)
|
||||
if err := unmarshal(b, &in); err != nil {
|
||||
return toolsCallResult{}, err
|
||||
}
|
||||
res, err := tools.SecuritySearch(ctx, in)
|
||||
return formatResult(res, err)
|
||||
default:
|
||||
return toolsCallResult{}, fmt.Errorf("unknown tool: %s", params.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func formatResult(res any, err error) (toolsCallResult, error) {
|
||||
if err != nil {
|
||||
return toolsCallResult{Content: []contentItem{{Type: "text", Text: err.Error()}}, IsError: true}, nil
|
||||
}
|
||||
b, _ := json.MarshalIndent(res, "", " ")
|
||||
return toolsCallResult{Content: []contentItem{{Type: "text", Text: string(b)}}, IsError: false}, nil
|
||||
}
|
||||
|
||||
func writeRPCResult(w *bufio.Writer, id json.RawMessage, result any) error {
|
||||
resp := rpcResponse{JSONRPC: "2.0", ID: id, Result: result}
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write(append(b, '\n')); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
func writeRPCError(w *bufio.Writer, id json.RawMessage, code int, msg string, data any) error {
|
||||
resp := rpcResponse{JSONRPC: "2.0", ID: id, Error: &rpcError{Code: code, Message: msg, Data: data}}
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write(append(b, '\n')); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
func toolList() []toolDef {
|
||||
return []toolDef{
|
||||
{
|
||||
Name: "dev_log_upsert",
|
||||
Title: "Dev Log Upsert",
|
||||
Description: "Upsert a dev_log entry (title/body/metadata)",
|
||||
InputSchema: schemaObject(map[string]any{
|
||||
"id": schemaString("UUID"),
|
||||
"title": schemaString("Title"),
|
||||
"body": schemaString("Body"),
|
||||
"ts": schemaString("ISO8601 timestamp"),
|
||||
"tags": schemaArray(schemaString("Tag")),
|
||||
"project": schemaString("Project name"),
|
||||
"source": schemaString("Source (manual/git/cursor/terminal)"),
|
||||
"author": schemaString("Author"),
|
||||
"visibility": schemaString("public/private/confidential"),
|
||||
"content_hash": schemaString("sha256:<hex>"),
|
||||
}, []string{"id", "title", "body", "ts"}),
|
||||
},
|
||||
{
|
||||
Name: "dev_log_search",
|
||||
Title: "Dev Log Search",
|
||||
Description: "Search dev_log entries",
|
||||
InputSchema: schemaObject(map[string]any{
|
||||
"query": schemaString("Query"),
|
||||
"top_k": schemaNumber("Top K (default 5)"),
|
||||
"filter": map[string]any{
|
||||
"type": "object",
|
||||
"description": "Optional metadata filter (MCP filter DSL)",
|
||||
},
|
||||
}, []string{"query"}),
|
||||
},
|
||||
{
|
||||
Name: "trade_event_upsert",
|
||||
Title: "Trade Event Upsert",
|
||||
Description: "Upsert a trade_event entry",
|
||||
InputSchema: schemaObject(map[string]any{
|
||||
"id": schemaString("UUID"),
|
||||
"event_date": schemaString("YYYY-MM-DD"),
|
||||
"partner_name": schemaString("Partner name"),
|
||||
"event_type": schemaString("見積/受注/請求/入金/支払/発注"),
|
||||
"amount": schemaNumber("Amount"),
|
||||
"currency": schemaString("Currency (default JPY)"),
|
||||
"summary": schemaString("Short summary"),
|
||||
"detail": map[string]any{"type": "object", "description": "Detail JSON"},
|
||||
"doc_path": schemaString("Document path"),
|
||||
"tags": schemaArray(schemaString("Tag")),
|
||||
"direction": schemaString("in/out"),
|
||||
"amount_signed": schemaNumber("Signed amount"),
|
||||
"counterparty_id": schemaString("Optional company id"),
|
||||
}, []string{"id", "event_date", "partner_name", "event_type", "summary"}),
|
||||
},
|
||||
{
|
||||
Name: "trade_event_search",
|
||||
Title: "Trade Event Search",
|
||||
Description: "Search trade_event entries",
|
||||
InputSchema: schemaObject(map[string]any{
|
||||
"query": schemaString("Query"),
|
||||
"top_k": schemaNumber("Top K (default 5)"),
|
||||
"filter": map[string]any{
|
||||
"type": "object",
|
||||
"description": "Optional metadata filter (MCP filter DSL)",
|
||||
},
|
||||
}, []string{"query"}),
|
||||
},
|
||||
{
|
||||
Name: "doc_upsert",
|
||||
Title: "Doc Upsert",
|
||||
Description: "Upsert a doc entry",
|
||||
InputSchema: schemaObject(map[string]any{
|
||||
"id": schemaString("UUID"),
|
||||
"title": schemaString("Title"),
|
||||
"ts": schemaString("ISO8601 timestamp"),
|
||||
"tags": schemaArray(schemaString("Tag")),
|
||||
"source": schemaString("Source (pdf_summary/manual)"),
|
||||
"doc_path": schemaString("Document path"),
|
||||
"body": schemaString("Body"),
|
||||
}, []string{"id", "title", "body"}),
|
||||
},
|
||||
{
|
||||
Name: "doc_search",
|
||||
Title: "Doc Search",
|
||||
Description: "Search doc entries",
|
||||
InputSchema: schemaObject(map[string]any{
|
||||
"query": schemaString("Query"),
|
||||
"top_k": schemaNumber("Top K (default 5)"),
|
||||
"filter": map[string]any{
|
||||
"type": "object",
|
||||
"description": "Optional metadata filter (MCP filter DSL)",
|
||||
},
|
||||
}, []string{"query"}),
|
||||
},
|
||||
{
|
||||
Name: "security_upsert",
|
||||
Title: "Security Upsert",
|
||||
Description: "Upsert a security entry",
|
||||
InputSchema: schemaObject(map[string]any{
|
||||
"id": schemaString("UUID"),
|
||||
"ts": schemaString("ISO8601 timestamp"),
|
||||
"tags": schemaArray(schemaString("Tag")),
|
||||
"source": schemaString("Source (fail2ban/cerberus)"),
|
||||
"host": schemaString("Host name"),
|
||||
"severity": schemaString("info/warn/error"),
|
||||
"body": schemaString("Message"),
|
||||
}, []string{"id", "body", "ts"}),
|
||||
},
|
||||
{
|
||||
Name: "security_search",
|
||||
Title: "Security Search",
|
||||
Description: "Search security entries",
|
||||
InputSchema: schemaObject(map[string]any{
|
||||
"query": schemaString("Query"),
|
||||
"top_k": schemaNumber("Top K (default 5)"),
|
||||
"filter": map[string]any{
|
||||
"type": "object",
|
||||
"description": "Optional metadata filter (MCP filter DSL)",
|
||||
},
|
||||
}, []string{"query"}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schemaObject(props map[string]any, required []string) map[string]any {
|
||||
m := map[string]any{
|
||||
"type": "object",
|
||||
"properties": props,
|
||||
}
|
||||
if len(required) > 0 {
|
||||
m["required"] = required
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func schemaString(desc string) map[string]any {
|
||||
return map[string]any{"type": "string", "description": desc}
|
||||
}
|
||||
|
||||
func schemaNumber(desc string) map[string]any {
|
||||
return map[string]any{"type": "number", "description": desc}
|
||||
}
|
||||
|
||||
func schemaArray(items map[string]any) map[string]any {
|
||||
return map[string]any{"type": "array", "items": items}
|
||||
}
|
||||
337
docs/mcp_dev_memo.md
Normal file
337
docs/mcp_dev_memo.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# MCP 開発メモ (pgvecterAPI)
|
||||
|
||||
## 目的
|
||||
- MCP サーバー (Go) から pgvecterAPI を使って「開発日記」「雑談日記」「取引情報」の読み書き・検索を行う。
|
||||
- 使用するコレクション: `dev_diary`, `chitchat_diary`, `trade_event`
|
||||
|
||||
## 前提
|
||||
- pgvecterAPI が稼働中 (例: https://labo.sunamura-llc.com/pgvecter/)
|
||||
- API キーを発行済み (`X-API-KEY: pgv_...`)
|
||||
- 管理用は `X-ADMIN-API-KEY` を別途保持
|
||||
|
||||
## コレクション設計
|
||||
- `dev_diary`: 開発日記 (Codex 接続、読み書き・検索)
|
||||
- `chitchat_diary`: カスタム GPT の雑談日記 (読み書き・検索)
|
||||
- `trade_event`: 自前 AI の取引情報 (読み書き・検索)
|
||||
- `doc`: PDF 要約など一般文書
|
||||
- `security`: Cerberus / Fail2Ban / 監視ログ
|
||||
|
||||
## メタデータ方針
|
||||
共通キー案:
|
||||
- `source`: `codex` / `custom_gpt` / `internal_ai`
|
||||
- `author`: ユーザー名 or サービス名
|
||||
- `topic`: 任意 (例: `infra`, `api`, `trading`)
|
||||
- `ts`: ISO8601 (`2026-02-06T20:52:31+09:00`)
|
||||
- `tags`: 文字列配列
|
||||
|
||||
例:
|
||||
```json
|
||||
{
|
||||
"source": "codex",
|
||||
"author": "hideyuki",
|
||||
"topic": "deploy",
|
||||
"ts": "2026-02-06T21:30:00+09:00",
|
||||
"tags": ["pgvecter", "mcp"]
|
||||
}
|
||||
```
|
||||
|
||||
## API 仕様 (最低限)
|
||||
- `POST /kb/upsert`:
|
||||
- `id` は UUID 必須
|
||||
- `collection` に `dev_diary` / `chitchat_diary` / `trade_event`
|
||||
- `content` が本文
|
||||
- `metadata` に上記メタデータ
|
||||
- `embedding` 省略時は llama.cpp で自動生成
|
||||
|
||||
- `POST /kb/search`:
|
||||
- `collection` を指定
|
||||
- `query` で検索
|
||||
- `top_k` を指定
|
||||
- `filter` で metadata フィルタ可能
|
||||
|
||||
## curl 例 (dev_diary)
|
||||
### upsert
|
||||
```bash
|
||||
curl -s -X POST https://labo.sunamura-llc.com/pgvecter/kb/upsert \
|
||||
-H "X-API-KEY: pgv_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id":"00000000-0000-0000-0000-000000000001",
|
||||
"collection":"dev_diary",
|
||||
"content":"MCP の設計を整理した。",
|
||||
"metadata":{
|
||||
"source":"codex",
|
||||
"author":"hideyuki",
|
||||
"topic":"mcp",
|
||||
"ts":"2026-02-06T21:30:00+09:00",
|
||||
"tags":["design","memo"]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### search
|
||||
```bash
|
||||
curl -s -X POST https://labo.sunamura-llc.com/pgvecter/kb/search \
|
||||
-H "X-API-KEY: pgv_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"collection":"dev_diary",
|
||||
"query":"MCP 設計のメモ",
|
||||
"top_k":5
|
||||
}'
|
||||
```
|
||||
|
||||
## MCP サーバー (Go) 実装方針
|
||||
- ツール設計例
|
||||
- `diary_upsert(collection, text, metadata)`
|
||||
- `diary_search(collection, query, top_k, filter)`
|
||||
- UUID 生成: `github.com/google/uuid` など
|
||||
- HTTP クライアント: 標準 `net/http`
|
||||
- `X-API-KEY` は MCP サーバーの環境変数から注入
|
||||
|
||||
## 追加メモ
|
||||
- 1リクエスト1ドキュメント upsert なので、複数件を扱う場合は MCP 側でループ
|
||||
- `permissions.users` はサーバー側で強制注入される前提
|
||||
- 収集対象別の collection により検索が明確になる
|
||||
|
||||
|
||||
## dev_log スキーマとマッピング
|
||||
|
||||
**想定フィールド**
|
||||
- `id` UUID
|
||||
- `ts` timestamp
|
||||
- `title` text
|
||||
- `body` text
|
||||
- `tags` text[]
|
||||
- `project` text
|
||||
- `source` text
|
||||
- `author` text
|
||||
- `visibility` text (`public` / `private` / `confidential`)
|
||||
- `content_hash` text (SHA256)
|
||||
- `embedding` vector(1024) (bge-m3 固定)
|
||||
|
||||
**pgvecterAPI へのマッピング**
|
||||
- `id` -> `id` (UUID 必須)
|
||||
- `body` -> `content`
|
||||
- `title` -> `metadata.title` (必要なら content 先頭に埋め込み)
|
||||
- `ts` -> `metadata.ts` (ISO8601)
|
||||
- `tags` -> `metadata.tags`
|
||||
- `project` -> `metadata.project`
|
||||
- `source` -> `metadata.source`
|
||||
- `author` -> `metadata.author`
|
||||
- `visibility` -> `metadata.visibility`
|
||||
- `content_hash` -> `metadata.content_hash`
|
||||
- `embedding` -> 省略して自動生成 (bge-m3 固定運用)
|
||||
|
||||
**コレクション名**
|
||||
- `dev_log`
|
||||
|
||||
**content_hash 生成ルール(採用)**
|
||||
- 対象: `title` と `body`
|
||||
- 連結: `title + \"\\n\" + body`
|
||||
- 正規化: 先頭/末尾の空白を trim、改行を LF に統一
|
||||
- ハッシュ: SHA-256
|
||||
- 保存形式: `sha256:<hex>`
|
||||
|
||||
## dev_log テンプレ
|
||||
|
||||
### upsert (dev_log)
|
||||
```bash
|
||||
curl -s -X POST https://labo.sunamura-llc.com/pgvecter/kb/upsert \
|
||||
-H "X-API-KEY: pgv_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id":"00000000-0000-0000-0000-000000000001",
|
||||
"collection":"dev_log",
|
||||
"content":"【タイトル】\n本文をここに書く。",
|
||||
"metadata":{
|
||||
"title":"タイトル",
|
||||
"ts":"2026-02-06T21:30:00+09:00",
|
||||
"tags":["llama.cpp","pgvector"],
|
||||
"project":"pgvecterAPI",
|
||||
"source":"terminal",
|
||||
"author":"hideyuki",
|
||||
"visibility":"private",
|
||||
"content_hash":"sha256:..."
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### search (dev_log)
|
||||
```bash
|
||||
curl -s -X POST https://labo.sunamura-llc.com/pgvecter/kb/search \
|
||||
-H "X-API-KEY: pgv_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"collection":"dev_log",
|
||||
"query":"埋め込みと検索の設計",
|
||||
"top_k":5,
|
||||
"filter": {
|
||||
"and":[
|
||||
{"eq": {"metadata.project":"pgvecterAPI"}},
|
||||
{"contains": {"metadata.tags":"pgvector"}}
|
||||
]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## trade_event スキーマと補足
|
||||
|
||||
**想定フィールド**
|
||||
- `id` UUID
|
||||
- `event_date` date
|
||||
- `partner_name` text
|
||||
- `event_type` text
|
||||
- `amount` numeric
|
||||
- `currency` text (default `JPY`)
|
||||
- `summary` text
|
||||
- `detail` jsonb
|
||||
- `doc_path` text
|
||||
- `tags` text[]
|
||||
- `embedding` vector(1024) (summary + detail をまとめて埋める)
|
||||
|
||||
**補足フィールド (採用)**
|
||||
- `direction` text (`in` / `out`)
|
||||
- `amount_signed` numeric (入金=プラス / 支払=マイナス)
|
||||
- `counterparty_id` text (将来の company_master 連携用, nullable)
|
||||
|
||||
**pgvecterAPI へのマッピング**
|
||||
- `id` -> `id` (UUID 必須)
|
||||
- `summary` + `detail` -> `content` (summary + detail の主要項目をテキスト化)
|
||||
- `event_date` -> `metadata.event_date` (YYYY-MM-DD)
|
||||
- `partner_name` -> `metadata.partner_name`
|
||||
- `event_type` -> `metadata.event_type`
|
||||
- `amount` -> `metadata.amount`
|
||||
- `currency` -> `metadata.currency`
|
||||
- `doc_path` -> `metadata.doc_path`
|
||||
- `tags` -> `metadata.tags`
|
||||
- `detail` -> `metadata.detail` (JSON をそのまま格納)
|
||||
- `direction` -> `metadata.direction`
|
||||
- `amount_signed` -> `metadata.amount_signed`
|
||||
- `counterparty_id` -> `metadata.counterparty_id`
|
||||
- `embedding` -> 省略して自動生成 (bge-m3 固定運用)
|
||||
|
||||
**コレクション名**
|
||||
- `trade_event`
|
||||
|
||||
## trade_event テンプレ
|
||||
|
||||
### upsert (trade_event)
|
||||
```bash
|
||||
curl -s -X POST https://labo.sunamura-llc.com/pgvecter/kb/upsert \
|
||||
-H "X-API-KEY: pgv_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id":"00000000-0000-0000-0000-000000000002",
|
||||
"collection":"trade_event",
|
||||
"content":"【見積】A社 2026-01-15\n50万円の見積。担当:山田。参照: Q-2026-001。",
|
||||
"metadata":{
|
||||
"event_date":"2026-01-15",
|
||||
"partner_name":"A社",
|
||||
"event_type":"見積",
|
||||
"amount":500000,
|
||||
"currency":"JPY",
|
||||
"summary":"A社向け見積 50万円",
|
||||
"detail":{
|
||||
"reference":"Q-2026-001",
|
||||
"owner":"山田",
|
||||
"notes":"条件: 2ヶ月以内の発注"
|
||||
},
|
||||
"doc_path":"/nextcloud/trade/A社/Q-2026-001.pdf",
|
||||
"tags":["見積","A社"],
|
||||
"direction":"in",
|
||||
"amount_signed":500000,
|
||||
"counterparty_id":null
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### search (trade_event)
|
||||
```bash
|
||||
curl -s -X POST https://labo.sunamura-llc.com/pgvecter/kb/search \
|
||||
-H "X-API-KEY: pgv_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"collection":"trade_event",
|
||||
"query":"A社 去年の揉めた件",
|
||||
"top_k":5,
|
||||
"filter": {
|
||||
"and":[
|
||||
{"eq": {"metadata.partner_name":"A社"}},
|
||||
{"contains": {"metadata.tags":"揉めた"}}
|
||||
]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## doc テンプレ
|
||||
|
||||
### upsert (doc)
|
||||
```bash
|
||||
curl -s -X POST https://labo.sunamura-llc.com/pgvecter/kb/upsert \
|
||||
-H "X-API-KEY: pgv_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id":"00000000-0000-0000-0000-000000000003",
|
||||
"collection":"doc",
|
||||
"content":"【契約書要約】A社\n契約期間は2026年4月〜2027年3月。解約は30日前通知。",
|
||||
"metadata":{
|
||||
"title":"A社 取引基本契約書",
|
||||
"ts":"2026-02-06T22:00:00+09:00",
|
||||
"tags":["契約","A社"],
|
||||
"source":"pdf_summary",
|
||||
"doc_path":"/nextcloud/docs/contract_A_2026.pdf"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### search (doc)
|
||||
```bash
|
||||
curl -s -X POST https://labo.sunamura-llc.com/pgvecter/kb/search \
|
||||
-H "X-API-KEY: pgv_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"collection":"doc",
|
||||
"query":"A社 契約 更新条件",
|
||||
"top_k":5,
|
||||
"filter": {
|
||||
"contains": {"metadata.tags":"契約"}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## security テンプレ
|
||||
|
||||
### upsert (security)
|
||||
```bash
|
||||
curl -s -X POST https://labo.sunamura-llc.com/pgvecter/kb/upsert \
|
||||
-H "X-API-KEY: pgv_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id":"00000000-0000-0000-0000-000000000004",
|
||||
"collection":"security",
|
||||
"content":"Fail2Ban: 203.0.113.10 を 10分 ban。理由: SSH brute force。",
|
||||
"metadata":{
|
||||
"ts":"2026-02-06T22:05:00+09:00",
|
||||
"tags":["fail2ban","ssh"],
|
||||
"source":"fail2ban",
|
||||
"host":"x85-131-243-202",
|
||||
"severity":"warn"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### search (security)
|
||||
```bash
|
||||
curl -s -X POST https://labo.sunamura-llc.com/pgvecter/kb/search \
|
||||
-H "X-API-KEY: pgv_..." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"collection":"security",
|
||||
"query":"SSH brute force の最近の事例",
|
||||
"top_k":5,
|
||||
"filter": {
|
||||
"contains": {"metadata.tags":"ssh"}
|
||||
}
|
||||
}'
|
||||
```
|
||||
219
internal/mcp/tools.go
Normal file
219
internal/mcp/tools.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"pgvecterapi/internal/pgvecter"
|
||||
)
|
||||
|
||||
type ToolSet struct {
|
||||
Client *pgvecter.Client
|
||||
}
|
||||
|
||||
func NewToolSet(client *pgvecter.Client) *ToolSet {
|
||||
return &ToolSet{Client: client}
|
||||
}
|
||||
|
||||
type DevLogUpsertInput struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
TS string `json:"ts"`
|
||||
Tags []string `json:"tags"`
|
||||
Project string `json:"project"`
|
||||
Source string `json:"source"`
|
||||
Author string `json:"author"`
|
||||
Visibility string `json:"visibility"`
|
||||
ContentHash string `json:"content_hash"`
|
||||
}
|
||||
|
||||
type DevLogSearchInput struct {
|
||||
Query string `json:"query"`
|
||||
TopK int `json:"top_k"`
|
||||
Filter map[string]any `json:"filter"`
|
||||
}
|
||||
|
||||
type TradeEventUpsertInput struct {
|
||||
ID string `json:"id"`
|
||||
EventDate string `json:"event_date"`
|
||||
PartnerName string `json:"partner_name"`
|
||||
EventType string `json:"event_type"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
Summary string `json:"summary"`
|
||||
Detail map[string]any `json:"detail"`
|
||||
DocPath string `json:"doc_path"`
|
||||
Tags []string `json:"tags"`
|
||||
Direction string `json:"direction"`
|
||||
AmountSigned float64 `json:"amount_signed"`
|
||||
CounterpartyID string `json:"counterparty_id"`
|
||||
}
|
||||
|
||||
type TradeEventSearchInput struct {
|
||||
Query string `json:"query"`
|
||||
TopK int `json:"top_k"`
|
||||
Filter map[string]any `json:"filter"`
|
||||
}
|
||||
|
||||
type DocUpsertInput struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
TS string `json:"ts"`
|
||||
Tags []string `json:"tags"`
|
||||
Source string `json:"source"`
|
||||
DocPath string `json:"doc_path"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
type DocSearchInput struct {
|
||||
Query string `json:"query"`
|
||||
TopK int `json:"top_k"`
|
||||
Filter map[string]any `json:"filter"`
|
||||
}
|
||||
|
||||
type SecurityUpsertInput struct {
|
||||
ID string `json:"id"`
|
||||
TS string `json:"ts"`
|
||||
Tags []string `json:"tags"`
|
||||
Source string `json:"source"`
|
||||
Host string `json:"host"`
|
||||
Severity string `json:"severity"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
type SecuritySearchInput struct {
|
||||
Query string `json:"query"`
|
||||
TopK int `json:"top_k"`
|
||||
Filter map[string]any `json:"filter"`
|
||||
}
|
||||
|
||||
func (t *ToolSet) DevLogUpsert(ctx context.Context, in DevLogUpsertInput) (*pgvecter.UpsertResponse, error) {
|
||||
content := "【" + in.Title + "】\n" + in.Body
|
||||
metadata := map[string]any{
|
||||
"title": in.Title,
|
||||
"ts": in.TS,
|
||||
"tags": in.Tags,
|
||||
"project": in.Project,
|
||||
"source": in.Source,
|
||||
"author": in.Author,
|
||||
"visibility": in.Visibility,
|
||||
"content_hash": in.ContentHash,
|
||||
}
|
||||
return t.Client.Upsert(ctx, pgvecter.UpsertRequest{
|
||||
ID: in.ID,
|
||||
Collection: "dev_log",
|
||||
Content: content,
|
||||
Metadata: metadata,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ToolSet) DevLogSearch(ctx context.Context, in DevLogSearchInput) (*pgvecter.SearchResponse, error) {
|
||||
if in.TopK == 0 {
|
||||
in.TopK = 5
|
||||
}
|
||||
return t.Client.Search(ctx, pgvecter.SearchRequest{
|
||||
Query: in.Query,
|
||||
TopK: in.TopK,
|
||||
Collection: "dev_log",
|
||||
Filter: in.Filter,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ToolSet) TradeEventUpsert(ctx context.Context, in TradeEventUpsertInput) (*pgvecter.UpsertResponse, error) {
|
||||
content := "【" + in.EventType + "】" + in.PartnerName + " " + in.EventDate + "\n" + in.Summary
|
||||
metadata := map[string]any{
|
||||
"event_date": in.EventDate,
|
||||
"partner_name": in.PartnerName,
|
||||
"event_type": in.EventType,
|
||||
"amount": in.Amount,
|
||||
"currency": in.Currency,
|
||||
"summary": in.Summary,
|
||||
"detail": in.Detail,
|
||||
"doc_path": in.DocPath,
|
||||
"tags": in.Tags,
|
||||
"direction": in.Direction,
|
||||
"amount_signed": in.AmountSigned,
|
||||
"counterparty_id": in.CounterpartyID,
|
||||
}
|
||||
return t.Client.Upsert(ctx, pgvecter.UpsertRequest{
|
||||
ID: in.ID,
|
||||
Collection: "trade_event",
|
||||
Content: content,
|
||||
Metadata: metadata,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ToolSet) TradeEventSearch(ctx context.Context, in TradeEventSearchInput) (*pgvecter.SearchResponse, error) {
|
||||
if in.TopK == 0 {
|
||||
in.TopK = 5
|
||||
}
|
||||
return t.Client.Search(ctx, pgvecter.SearchRequest{
|
||||
Query: in.Query,
|
||||
TopK: in.TopK,
|
||||
Collection: "trade_event",
|
||||
Filter: in.Filter,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ToolSet) DocUpsert(ctx context.Context, in DocUpsertInput) (*pgvecter.UpsertResponse, error) {
|
||||
content := "【" + in.Title + "】\n" + in.Body
|
||||
metadata := map[string]any{
|
||||
"title": in.Title,
|
||||
"ts": in.TS,
|
||||
"tags": in.Tags,
|
||||
"source": in.Source,
|
||||
"doc_path": in.DocPath,
|
||||
}
|
||||
return t.Client.Upsert(ctx, pgvecter.UpsertRequest{
|
||||
ID: in.ID,
|
||||
Collection: "doc",
|
||||
Content: content,
|
||||
Metadata: metadata,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ToolSet) DocSearch(ctx context.Context, in DocSearchInput) (*pgvecter.SearchResponse, error) {
|
||||
if in.TopK == 0 {
|
||||
in.TopK = 5
|
||||
}
|
||||
return t.Client.Search(ctx, pgvecter.SearchRequest{
|
||||
Query: in.Query,
|
||||
TopK: in.TopK,
|
||||
Collection: "doc",
|
||||
Filter: in.Filter,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ToolSet) SecurityUpsert(ctx context.Context, in SecurityUpsertInput) (*pgvecter.UpsertResponse, error) {
|
||||
content := in.Body
|
||||
metadata := map[string]any{
|
||||
"ts": in.TS,
|
||||
"tags": in.Tags,
|
||||
"source": in.Source,
|
||||
"host": in.Host,
|
||||
"severity": in.Severity,
|
||||
}
|
||||
return t.Client.Upsert(ctx, pgvecter.UpsertRequest{
|
||||
ID: in.ID,
|
||||
Collection: "security",
|
||||
Content: content,
|
||||
Metadata: metadata,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *ToolSet) SecuritySearch(ctx context.Context, in SecuritySearchInput) (*pgvecter.SearchResponse, error) {
|
||||
if in.TopK == 0 {
|
||||
in.TopK = 5
|
||||
}
|
||||
return t.Client.Search(ctx, pgvecter.SearchRequest{
|
||||
Query: in.Query,
|
||||
TopK: in.TopK,
|
||||
Collection: "security",
|
||||
Filter: in.Filter,
|
||||
})
|
||||
}
|
||||
|
||||
func NowISO() string {
|
||||
return time.Now().Format(time.RFC3339)
|
||||
}
|
||||
94
internal/pgvecter/client.go
Normal file
94
internal/pgvecter/client.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package pgvecter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
func NewClient(baseURL, apiKey string) *Client {
|
||||
return &Client{
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
APIKey: apiKey,
|
||||
HTTP: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Upsert(ctx context.Context, req UpsertRequest) (*UpsertResponse, error) {
|
||||
if req.ID == "" {
|
||||
return nil, errors.New("id is required")
|
||||
}
|
||||
if req.Content == "" {
|
||||
return nil, errors.New("content is required")
|
||||
}
|
||||
var resp UpsertResponse
|
||||
if err := c.postJSON(ctx, "/kb/upsert", req, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) Search(ctx context.Context, req SearchRequest) (*SearchResponse, error) {
|
||||
if req.Query == "" && len(req.QueryEmbedding) == 0 {
|
||||
return nil, errors.New("query or query_embedding is required")
|
||||
}
|
||||
var resp SearchResponse
|
||||
if err := c.postJSON(ctx, "/kb/search", req, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) postJSON(ctx context.Context, path string, body any, out any) error {
|
||||
buf, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url := c.BaseURL + path
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(buf))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if c.APIKey != "" {
|
||||
req.Header.Set("X-API-KEY", c.APIKey)
|
||||
}
|
||||
|
||||
res, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||
var er ErrorResponse
|
||||
if json.Unmarshal(data, &er) == nil && er.Error != "" {
|
||||
return fmt.Errorf("pgvecter error: %s (%s)", er.Error, er.Code)
|
||||
}
|
||||
return fmt.Errorf("pgvecter http %d: %s", res.StatusCode, strings.TrimSpace(string(data)))
|
||||
}
|
||||
|
||||
if out == nil {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, out)
|
||||
}
|
||||
52
internal/pgvecter/types.go
Normal file
52
internal/pgvecter/types.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package pgvecter
|
||||
|
||||
type UpsertRequest struct {
|
||||
ID string `json:"id"`
|
||||
DocID string `json:"doc_id,omitempty"`
|
||||
ChunkIndex int `json:"chunk_index,omitempty"`
|
||||
Collection string `json:"collection,omitempty"`
|
||||
AutoChunk bool `json:"auto_chunk,omitempty"`
|
||||
ChunkSize int `json:"chunk_size,omitempty"`
|
||||
ChunkOver int `json:"chunk_overlap,omitempty"`
|
||||
Content string `json:"content"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
Embedding []float64 `json:"embedding,omitempty"`
|
||||
}
|
||||
|
||||
type UpsertResponse struct {
|
||||
ID string `json:"id"`
|
||||
DocID string `json:"doc_id"`
|
||||
Updated bool `json:"updated"`
|
||||
Chunks []UpsertChunk `json:"chunks,omitempty"`
|
||||
}
|
||||
|
||||
type UpsertChunk struct {
|
||||
ID string `json:"id"`
|
||||
ChunkIndex int `json:"chunk_index"`
|
||||
}
|
||||
|
||||
type SearchRequest struct {
|
||||
Query string `json:"query"`
|
||||
QueryEmbedding []float64 `json:"query_embedding,omitempty"`
|
||||
TopK int `json:"top_k,omitempty"`
|
||||
Collection string `json:"collection,omitempty"`
|
||||
Collections []string `json:"collections,omitempty"`
|
||||
Filter map[string]any `json:"filter,omitempty"`
|
||||
}
|
||||
|
||||
type SearchResponse struct {
|
||||
Results []SearchResult `json:"results"`
|
||||
}
|
||||
|
||||
type SearchResult struct {
|
||||
ID string `json:"id"`
|
||||
Collection string `json:"collection"`
|
||||
Content string `json:"content"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
Score float64 `json:"score"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
BIN
pgvecterapi
Executable file
BIN
pgvecterapi
Executable file
Binary file not shown.
Reference in New Issue
Block a user