addrkit: strict ordinal matching, LLM-variant guard, externalized prompt
- match: require candidate ordinal to equal the query ordinal in fuzzy search (deterministic typo recall 73% -> 80%, no wrong houses) - resolver: compatibleVariant guard — LLM may only fix street spelling, keeping the same words, ordinal and house unit; damerau distance for transpositions - llm: prompt moved out of code (go:embed default_prompt.txt) with llm.system_prompt / llm.system_prompt_file overrides; guided JSON parsing tolerant of thinking blocks - address: recognize ordinals appearing after the street name - tests: first coverage for the LLM adapter and config prompt loading; README Measured (sample 200): deterministic 94.7-95.8% exact / 0 wrong across 4 seeds; LLM+guard typo 90.7-89.8% / 0 wrong. LLM fallback latency ~1.2-1.9s.
This commit is contained in:
@@ -46,6 +46,18 @@ type LLMConfig struct {
|
||||
Token string `json:"token"`
|
||||
Model string `json:"model"`
|
||||
TimeoutSeconds int `json:"timeout_seconds"`
|
||||
// MaxTokens bounds the rewriter's reply. Thinking models need enough room
|
||||
// for the final JSON even when thinking is disabled.
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
// DisableThinking turns off chain-of-thought for models that support it
|
||||
// (Qwen3 via chat_template_kwargs). A rewriter only needs the JSON answer.
|
||||
DisableThinking bool `json:"disable_thinking"`
|
||||
// SystemPrompt overrides the built-in rewriter prompt inline. When empty,
|
||||
// the prompt embedded in the binary is used.
|
||||
SystemPrompt string `json:"system_prompt"`
|
||||
// SystemPromptFile loads the rewriter prompt from a file. When set it takes
|
||||
// precedence over SystemPrompt, which keeps long prompts out of the config.
|
||||
SystemPromptFile string `json:"system_prompt_file"`
|
||||
}
|
||||
|
||||
// Default returns a config with sane defaults for the non-required fields.
|
||||
@@ -63,7 +75,9 @@ func Default() Config {
|
||||
Retries: 1,
|
||||
},
|
||||
LLM: LLMConfig{
|
||||
TimeoutSeconds: 3,
|
||||
TimeoutSeconds: 3,
|
||||
MaxTokens: 512,
|
||||
DisableThinking: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -79,6 +93,13 @@ func Load(path string) (Config, error) {
|
||||
if err := dec.Decode(&cfg); err != nil {
|
||||
return Config{}, fmt.Errorf("parse config: %w", err)
|
||||
}
|
||||
if cfg.LLM.SystemPromptFile != "" {
|
||||
data, err := os.ReadFile(cfg.LLM.SystemPromptFile)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("read llm.system_prompt_file: %w", err)
|
||||
}
|
||||
cfg.LLM.SystemPrompt = string(data)
|
||||
}
|
||||
if err := cfg.validate(); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeFile(t *testing.T, dir, name, content string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||
t.Fatalf("write %s: %v", name, err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func baseConfig(promptFields string) string {
|
||||
return `{
|
||||
"addr": ":6171",
|
||||
"data_path": "registry.db",
|
||||
"portal": { "url": "http://portal" },
|
||||
"llm": { "enabled": true, "url": "http://llm"` + promptFields + ` }
|
||||
}`
|
||||
}
|
||||
|
||||
func TestLoadInlineSystemPrompt(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := writeFile(t, dir, "config.json", baseConfig(`, "system_prompt": "INLINE PROMPT"`))
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.LLM.SystemPrompt != "INLINE PROMPT" {
|
||||
t.Errorf("SystemPrompt = %q", cfg.LLM.SystemPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSystemPromptFromFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
promptFile := writeFile(t, dir, "prompt.txt", "PROMPT FROM FILE\n")
|
||||
path := writeFile(t, dir, "config.json", baseConfig(`, "system_prompt_file": "`+promptFile+`"`))
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.LLM.SystemPrompt != "PROMPT FROM FILE\n" {
|
||||
t.Errorf("SystemPrompt = %q", cfg.LLM.SystemPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMissingSystemPromptFileFailsClosed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := writeFile(t, dir, "config.json", baseConfig(`, "system_prompt_file": "`+filepath.Join(dir, "nope.txt")+`"`))
|
||||
|
||||
if _, err := Load(path); err == nil {
|
||||
t.Fatal("expected error for missing system_prompt_file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRequiresPortalURL(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := writeFile(t, dir, "config.json", `{"addr": ":6171", "data_path": "x.db"}`)
|
||||
if _, err := Load(path); err == nil {
|
||||
t.Fatal("expected error for missing portal.url")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user