DEV-000286 Reject irrelevant research excerpts #264
@@ -24,8 +24,14 @@ var (
|
||||
bingLinkPattern = regexp.MustCompile(`(?is)<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>.*?<h2[^>]*><a[^>]*>(.*?)</a>`)
|
||||
bingSnippetPattern = regexp.MustCompile(`(?is)<li[^>]+class=["'][^"']*b_algo[^"']*["'][^>]*>.*?<p[^>]*>(.*?)</p>`)
|
||||
tagPattern = regexp.MustCompile(`(?s)<[^>]*>`)
|
||||
termPattern = regexp.MustCompile(`(?i)[\p{L}\p{N}]{2,}`)
|
||||
)
|
||||
|
||||
var searchStopTerms = map[string]struct{}{
|
||||
"какая": {}, "какое": {}, "какие": {}, "какой": {}, "что": {}, "как": {}, "где": {}, "когда": {}, "почему": {}, "мне": {}, "пожалуйста": {},
|
||||
"what": {}, "which": {}, "where": {}, "when": {}, "why": {}, "how": {}, "please": {}, "the": {}, "this": {},
|
||||
}
|
||||
|
||||
// SearchConnector uses a configured HTTPS search endpoint. It does not know
|
||||
// cities, weather terms, entities or model names: the complete semantic query
|
||||
// comes from the interpretation layer.
|
||||
@@ -91,7 +97,7 @@ func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSear
|
||||
snippet = cleanHTML(string(snippets[index][1]))
|
||||
}
|
||||
statement := strings.TrimSpace(strings.Trim(title+" — "+snippet, " —"))
|
||||
if statement == "" {
|
||||
if statement == "" || !searchStatementRelatesToQuery(request.Query, statement) {
|
||||
continue
|
||||
}
|
||||
ordinal := strconv.Itoa(index)
|
||||
@@ -108,6 +114,30 @@ func (c SearchConnector) Collect(ctx context.Context, request epistemics.WebSear
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// searchStatementRelatesToQuery is a bounded relevance gate, not a truth
|
||||
// check. It prevents generic search pages from becoming apparent evidence
|
||||
// when their content has no substantive relation to the requested query.
|
||||
func searchStatementRelatesToQuery(query, statement string) bool {
|
||||
terms := termPattern.FindAllString(strings.ToLower(query), -1)
|
||||
content := strings.ToLower(statement)
|
||||
usable := 0
|
||||
for _, term := range terms {
|
||||
if _, stop := searchStopTerms[term]; stop || len([]rune(term)) < 4 {
|
||||
continue
|
||||
}
|
||||
usable++
|
||||
runes := []rune(term)
|
||||
prefixLen := 5
|
||||
if len(runes) < prefixLen {
|
||||
prefixLen = len(runes)
|
||||
}
|
||||
if strings.Contains(content, string(runes[:prefixLen])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return usable == 0
|
||||
}
|
||||
|
||||
func (c SearchConnector) allowedHost(host string) bool { _, ok := c.AllowedHosts[host]; return ok }
|
||||
func (c SearchConnector) limit() int {
|
||||
if c.MaxResults <= 0 || c.MaxResults > 8 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000285-semantic-research-routing-fix.json",
|
||||
"task_id": "DEV-000285",
|
||||
"task_file": "dev/tasks/DEV-000286-research-result-relevance-filter.json",
|
||||
"task_id": "DEV-000286",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000286",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "b0a8b3e",
|
||||
"risk_tier": "E",
|
||||
"intent": "Reject web-search excerpts with no substantive lexical relation to the requested query so truth-first chat never presents irrelevant search results as useful evidence.",
|
||||
"requirement_ids": ["research relevance boundary", "truth-first no irrelevant evidence", "no entity-specific filtering"],
|
||||
"allowed_change_scope": {"modules": ["research adapter"], "paths": ["dev/ACTIVE_TASK.json", "dev/tasks/DEV-000286-research-result-relevance-filter.json", "adapters/research/search_connector.go", "tests/research_runtime/search_connector_test.go"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "database/**", "cmd/**", "deploy/**", "LLM-local/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["epistemics ResearchCandidate", "provider-neutral WebSearchRequest"], "may_change": []},
|
||||
"semantic_owner": "epistemics research adapter boundary",
|
||||
"canonical_state": ["no new canonical state; irrelevant candidates are not emitted"],
|
||||
"state_transitions": ["raw search result -> relevance gate -> candidate or omission"],
|
||||
"side_effects_and_privileges": ["read-only search connector only"],
|
||||
"persistence_and_migrations": ["none"],
|
||||
"failure_retry_restart": ["zero relevant candidates becomes the existing truthful no-evidence response"],
|
||||
"assumptions": ["short query terms without substantive tokens retain prior parser behavior for compatibility", "term overlap is a bounded relevance guard, not semantic truth verification"],
|
||||
"acceptance_scenarios": ["irrelevant dictionary results are omitted for a weather-like query", "relevant result is retained", "no entity-specific branch exists"],
|
||||
"required_tests": ["go test ./tests/research_runtime", "go vet ./adapters/research ./tests/research_runtime", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production health smoke", "one live chat smoke"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"]
|
||||
}
|
||||
@@ -16,7 +16,7 @@ func TestSearchConnectorReturnsBoundedImmutableEvidence(t *testing.T) {
|
||||
_, _ = w.Write([]byte(`<a class="result__a">Verified title</a><a class="result__snippet">Observed source excerpt</a>`))
|
||||
}))
|
||||
defer server.Close()
|
||||
request := epistemics.WebSearchRequest{Plan: epistemics.ResearchPlan{ID: "plan", Question: "current fact", SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"}, PropositionKey: "question-1", Query: "current fact"}
|
||||
request := epistemics.WebSearchRequest{Plan: epistemics.ResearchPlan{ID: "plan", Question: "verified source", SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"}, PropositionKey: "question-1", Query: "verified source"}
|
||||
connector := researchadapter.SearchConnector{EndpointTemplate: server.URL + "?q=%s", AllowedHosts: map[string]struct{}{"127.0.0.1": {}}, Client: server.Client(), MaxResults: 3}
|
||||
// The production connector requires HTTPS. The test server is HTTPS too;
|
||||
// use its host as the allowlisted value and preserve the same transport.
|
||||
@@ -52,3 +52,19 @@ func TestSearchConnectorParsesConfiguredBingStyleResults(t *testing.T) {
|
||||
t.Fatalf("unexpected Bing-style evidence: candidates=%+v err=%v", candidates, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchConnectorDropsIrrelevantResults(t *testing.T) {
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`<li class="b_algo"><h2><a href="https://source.example">Викисловарь</a></h2><p>Статья о грамматике</p></li>`))
|
||||
}))
|
||||
defer server.Close()
|
||||
request := epistemics.WebSearchRequest{Plan: epistemics.ResearchPlan{ID: "plan", Question: "какая погода в Киеве", SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"}, PropositionKey: "p", Query: "какая погода в Киеве"}
|
||||
connector := researchadapter.SearchConnector{EndpointTemplate: server.URL + "?q=%s", AllowedHosts: map[string]struct{}{"127.0.0.1": {}}, Client: server.Client(), MaxResults: 3}
|
||||
candidates, err := connector.Collect(context.Background(), request, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("collect search evidence: %v", err)
|
||||
}
|
||||
if len(candidates) != 0 {
|
||||
t.Fatalf("irrelevant result was retained: %+v", candidates)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user