feat: Auto-adjust chunk recall weights based on user feedback (#12689)

### What problem does this PR solve?

Implements automatic adjustment of knowledge base chunk recall weights
based on user feedback (upvotes/downvotes). When users upvote or
downvote a response, the system locates the corresponding knowledge
snippets and adjusts their recall weight to improve future retrieval
quality.

**Closes #12670**

**How it works:**
1. User upvotes/downvotes a response via `POST /thumbup`
2. System extracts chunk IDs from the conversation reference
3. For each referenced chunk:
   - Reads current `pagerank_fea` value from document store
   - Increments (+1) for upvote or decrements (-1) for downvote
   - Clamps weight to [0, 100] range
   - Updates chunk in ES/Infinity/OceanBase
4. Future retrievals score these chunks higher/lower based on
accumulated feedback

**Files changed:**
- `api/db/services/chunk_feedback_service.py` - New service for updating
chunk pagerank weights
- `api/apps/conversation_app.py` - Integrated feedback service into
thumbup endpoint
- `test/testcases/test_web_api/test_chunk_feedback/` - Unit tests

### Type of change

- [x] New Feature (non-breaking change which adds functionality)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Chat message feedback now updates per-chunk relevance weights
(feature-flag gated), with configurable weighting and atomic updates
across storage backends.

* **Bug Fixes**
* Stricter validation for message feedback inputs and more robust
handling of feedback transitions.

* **Tests**
* Expanded test coverage for chunk-feedback behavior, weighting
strategies, storage backends, and thumb-flip scenarios.

* **Chores**
  * CI workflow extended to run the new chunk-feedback web API tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: mkdev11 <YOUR_GITHUB_ID+MkDev11@users.noreply.github.com>
Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com>
This commit is contained in:
MkDev11
2026-04-07 18:52:18 -07:00
committed by GitHub
parent 4a2a17c27a
commit cfee2bc9db
11 changed files with 1293 additions and 13 deletions

View File

@@ -597,6 +597,84 @@ class InfinityConnection(InfinityConnectionBase):
self.connPool.release_conn(inf_conn)
return True
def adjust_chunk_pagerank_fea(
self,
chunk_id: str,
index_name: str,
knowledgebase_id: str,
delta: int,
min_weight: int,
max_weight: int,
row_id: int | None = None,
max_retries: int = 2,
) -> bool:
"""Adjust pagerank_fea on one chunk row in Infinity.
Uses row_id for a targeted update when available. If the row_id is
stale (concurrent update changed it), re-reads the current row_id and
retries up to *max_retries* times.
"""
table_name = f"{index_name}_{knowledgebase_id}"
for attempt in range(max_retries + 1):
inf_conn = self.connPool.get_conn()
try:
db_instance = inf_conn.get_database(self.dbName)
table_instance = db_instance.get_table(table_name)
if row_id is None:
df, _ = table_instance.output(
[PAGERANK_FLD, "row_id()"]
).filter(f"id = '{chunk_id}'").to_df()
if df.empty:
self.logger.warning(
"adjust_chunk_pagerank_fea: chunk %s not found in %s",
chunk_id, table_name,
)
return False
current_weight = int(float(df[PAGERANK_FLD].iloc[0] or 0))
row_id = int(df["row_id"].iloc[0])
else:
df, _ = table_instance.output(
[PAGERANK_FLD]
).filter(f"id = '{chunk_id}'").to_df()
if df.empty:
return False
current_weight = int(float(df[PAGERANK_FLD].iloc[0] or 0))
new_weight = max(min_weight, min(max_weight, current_weight + delta))
table_instance.update(
f"_row_id = {row_id}",
{PAGERANK_FLD: new_weight},
)
self.logger.info(
"adjust_chunk_pagerank_fea(chunk=%s, table=%s): %s -> %s via row_id=%s",
chunk_id, table_name, current_weight, new_weight, row_id,
)
return True
except InfinityException as e:
if attempt < max_retries:
self.logger.warning(
"adjust_chunk_pagerank_fea stale row_id=%s for chunk %s (attempt %s/%s): %s",
row_id, chunk_id, attempt + 1, max_retries, e,
)
row_id = None
continue
self.logger.error(
"adjust_chunk_pagerank_fea failed for chunk %s after %s attempts: %s",
chunk_id, max_retries + 1, e,
)
return False
except Exception as e:
self.logger.error(
"adjust_chunk_pagerank_fea error for chunk %s: %s", chunk_id, e,
)
return False
finally:
self.connPool.release_conn(inf_conn)
return False
"""
Helper functions for search result
"""