171 lines
5.6 KiB
Go
171 lines
5.6 KiB
Go
// graph_extraction_probe calls the production extractor without writing graph data.
|
|||
|
|
// The model configuration and AES key arrive on stdin and are never reported.
|
||
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"crypto/sha256"
|
||
|
|
"encoding/hex"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"os"
|
||
|
|
"time"
|
||
|
|
"unicode/utf8"
|
||
|
|
|
||
|
|
chatpipeline "github.com/Tencent/WeKnora/internal/application/service/chat_pipeline"
|
||
|
|
"github.com/Tencent/WeKnora/internal/models/chat"
|
||
|
|
"github.com/Tencent/WeKnora/internal/types"
|
||
|
|
"github.com/Tencent/WeKnora/internal/utils"
|
||
|
|
"gopkg.in/yaml.v3"
|
||
|
|
)
|
||
|
|
|
||
|
|
type request struct {
|
||
|
|
ExecutionID string `json:"execution_id"`
|
||
|
|
MaxHTTPSends *int `json:"max_http_sends"`
|
||
|
|
AESKey string `json:"aes_key"`
|
||
|
|
Model types.Model `json:"model"`
|
||
|
|
Chunk types.Chunk `json:"chunk"`
|
||
|
|
ExtractConfig types.ExtractConfig `json:"extract_config"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type chatDelegate interface{ chat.Chat }
|
||
|
|
|
||
|
|
type probeCall struct {
|
||
|
|
Number int `json:"number"`
|
||
|
|
ElapsedMS int64 `json:"elapsed_ms"`
|
||
|
|
MaxTokens int `json:"max_tokens"`
|
||
|
|
Thinking *bool `json:"thinking_requested"`
|
||
|
|
ResponseReceived bool `json:"response_received"`
|
||
|
|
Failed bool `json:"failed"`
|
||
|
|
FinishReason string `json:"finish_reason"`
|
||
|
|
ContentRunes int `json:"content_runes"`
|
||
|
|
ReasoningRunes int `json:"reasoning_runes"`
|
||
|
|
Usage types.TokenUsage `json:"usage"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type recordingChat struct {
|
||
|
|
chatDelegate
|
||
|
|
response *types.ChatResponse
|
||
|
|
calls int
|
||
|
|
records []probeCall
|
||
|
|
}
|
||
|
|
|
||
|
|
func (c *recordingChat) Chat(ctx context.Context, messages []chat.Message, options *chat.ChatOptions) (*types.ChatResponse, error) {
|
||
|
|
started := time.Now()
|
||
|
|
response, err := c.chatDelegate.Chat(ctx, messages, options)
|
||
|
|
c.response = response
|
||
|
|
c.calls++
|
||
|
|
record := probeCall{
|
||
|
|
Number: c.calls, ElapsedMS: time.Since(started).Milliseconds(),
|
||
|
|
ResponseReceived: response != nil, Failed: err != nil,
|
||
|
|
}
|
||
|
|
if options != nil {
|
||
|
|
record.MaxTokens = options.MaxTokens
|
||
|
|
if options.Thinking != nil {
|
||
|
|
thinking := *options.Thinking
|
||
|
|
record.Thinking = &thinking
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if response != nil {
|
||
|
|
record.FinishReason = response.FinishReason
|
||
|
|
record.ContentRunes = utf8.RuneCountInString(response.Content)
|
||
|
|
record.ReasoningRunes = utf8.RuneCountInString(response.ReasoningContent)
|
||
|
|
record.Usage = response.Usage
|
||
|
|
}
|
||
|
|
c.records = append(c.records, record)
|
||
|
|
return response, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func run() error {
|
||
|
|
var input request
|
||
|
|
if err := json.NewDecoder(os.Stdin).Decode(&input); err != nil {
|
||
|
|
return fmt.Errorf("invalid probe input")
|
||
|
|
}
|
||
|
|
capValue, capSet := os.LookupEnv("GRAPH_PROBE_MAX_SENDS")
|
||
|
|
maxSends, err := probeSendLimit(input.MaxHTTPSends, capValue, capSet)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if input.ExecutionID == "" || input.Chunk.ID == "" || input.Chunk.Content == "" || input.Model.ID == "" || input.Model.Name == "" {
|
||
|
|
return fmt.Errorf("probe requires a source chunk and model")
|
||
|
|
}
|
||
|
|
if err := os.Setenv("SYSTEM_AES_KEY", input.AESKey); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
input.AESKey = ""
|
||
|
|
key, err := utils.DecryptStoredSecret(input.Model.Parameters.APIKey)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("cannot decrypt configured model credential")
|
||
|
|
}
|
||
|
|
input.Model.Parameters.APIKey = key
|
||
|
|
defer os.Unsetenv("SYSTEM_AES_KEY")
|
||
|
|
chatConfig := chat.ConfigFromModel(&input.Model, "", "")
|
||
|
|
model, err := chat.NewChat(chatConfig, nil)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("cannot initialize configured remote model")
|
||
|
|
}
|
||
|
|
base, err := os.ReadFile("config/config.yaml")
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
var config struct {
|
||
|
|
Extract struct {
|
||
|
|
Graph struct {
|
||
|
|
Description string `yaml:"description"`
|
||
|
|
} `yaml:"extract_graph"`
|
||
|
|
} `yaml:"extract"`
|
||
|
|
}
|
||
|
|
if err := yaml.Unmarshal(base, &config); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
recorder := &recordingChat{chatDelegate: model}
|
||
|
|
extractor := chatpipeline.NewGraphExtractor(recorder, &types.PromptTemplateStructured{
|
||
|
|
Description: types.AppendCustomPromptInstructions(config.Extract.Graph.Description, input.ExtractConfig.CustomInstructions, "graph_extraction"),
|
||
|
|
Tags: input.ExtractConfig.Tags,
|
||
|
|
})
|
||
|
|
ctx, guard, cancel, err := bindProbeExecution(context.Background(), input.ExecutionID, input.Model.ID, maxSends)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer cancel()
|
||
|
|
started := time.Now()
|
||
|
|
graph, extractionErr := extractor.Extract(ctx, input.Chunk.Content)
|
||
|
|
hash := sha256.Sum256([]byte(input.Chunk.Content))
|
||
|
|
report := map[string]any{
|
||
|
|
"checked_at": started.UTC().Format(time.RFC3339), "elapsed_ms": time.Since(started).Milliseconds(),
|
||
|
|
"chunk_id": input.Chunk.ID, "source_sha256": hex.EncodeToString(hash[:]),
|
||
|
|
"model_name": input.Model.Name, "allowed_relation_types": input.ExtractConfig.Tags,
|
||
|
|
"passed": extractionErr == nil, "graph": graph, "production_write": false,
|
||
|
|
"model_calls": recorder.calls,
|
||
|
|
"execution_id": input.ExecutionID,
|
||
|
|
"http_send_reservations": guard.Reservations(),
|
||
|
|
"http_send_limit": maxSends,
|
||
|
|
"call_diagnostics": recorder.records,
|
||
|
|
"effective_thinking_control": chat.EffectiveThinkingControl(chatConfig),
|
||
|
|
"diagnostics_version": 2,
|
||
|
|
}
|
||
|
|
if recorder.response != nil {
|
||
|
|
report["finish_reason"] = recorder.response.FinishReason
|
||
|
|
report["usage"] = recorder.response.Usage
|
||
|
|
if extractionErr != nil {
|
||
|
|
report["rejected_model_content"] = recorder.response.Content
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if extractionErr != nil {
|
||
|
|
report["error"] = extractionErr.Error()
|
||
|
|
}
|
||
|
|
encoded, err := json.Marshal(report)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
fmt.Printf("GRAPH_EXTRACTION_PROBE_RESULT=%s\n", encoded)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
if err := run(); err != nil {
|
||
|
|
fmt.Fprintln(os.Stderr, err)
|
||
|
|
os.Exit(1)
|
||
|
|
}
|
||
|
|
}
|