-
Notifications
You must be signed in to change notification settings - Fork 4
Add automated downvote review system #68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,124 @@ | ||||||
| # Downvote Review | ||||||
|
|
||||||
| Review downvoted snippets, hide them, create corrective KB entries, and generate a report. | ||||||
|
|
||||||
| ## Steps | ||||||
|
|
||||||
| ### 1. Find unreviewed downvoted snippets | ||||||
|
|
||||||
| Use the Supabase MCP to query for downvoted snippets that haven't been processed yet: | ||||||
|
|
||||||
| ```sql | ||||||
| SELECT | ||||||
| s.id AS snippet_id, | ||||||
| s.title, | ||||||
| s.explanation, | ||||||
| s.disinformation_categories, | ||||||
| s.confidence_scores, | ||||||
| s.created_at, | ||||||
| drq.status AS queue_status, | ||||||
| COUNT(uls.id) FILTER (WHERE uls.value = -1) AS downvote_count | ||||||
| FROM snippets s | ||||||
| JOIN user_like_snippets uls ON uls.snippet = s.id | ||||||
| LEFT JOIN downvote_review_queue drq ON drq.snippet_id = s.id | ||||||
| WHERE uls.value = -1 | ||||||
| AND (drq.status IS NULL OR drq.status = 'pending' OR drq.status = 'error') | ||||||
| AND NOT EXISTS ( | ||||||
| SELECT 1 FROM user_hide_snippets uhs WHERE uhs.snippet = s.id | ||||||
| ) | ||||||
|
Comment on lines
+26
to
+28
|
||||||
| GROUP BY s.id, drq.status | ||||||
| ORDER BY s.created_at DESC; | ||||||
|
Comment on lines
+24
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This query filters out the very rows the new trigger creates. The new trigger hides snippets on the first downvote, so Suggested fix-SELECT
- s.id AS snippet_id,
- s.title,
- s.explanation,
- s.disinformation_categories,
- s.confidence_scores,
- s.created_at,
- drq.status AS queue_status,
- COUNT(uls.id) FILTER (WHERE uls.value = -1) AS downvote_count
-FROM snippets s
-JOIN user_like_snippets uls ON uls.snippet = s.id
-LEFT JOIN downvote_review_queue drq ON drq.snippet_id = s.id
-WHERE uls.value = -1
-AND (drq.status IS NULL OR drq.status = 'pending' OR drq.status = 'error')
-AND NOT EXISTS (
- SELECT 1 FROM user_hide_snippets uhs WHERE uhs.snippet = s.id
-)
-GROUP BY s.id, drq.status
-ORDER BY s.created_at DESC;
+SELECT
+ s.id AS snippet_id,
+ s.title,
+ s.explanation,
+ s.disinformation_categories,
+ s.confidence_scores,
+ s.created_at,
+ drq.status AS queue_status,
+ drq.downvoted_at
+FROM downvote_review_queue drq
+JOIN snippets s ON s.id = drq.snippet_id
+WHERE drq.status IN ('pending', 'error')
+ORDER BY drq.downvoted_at DESC;🤖 Prompt for AI Agents |
||||||
| ``` | ||||||
|
|
||||||
| If no results, check for completed reviews too: | ||||||
| ```sql | ||||||
| SELECT status, COUNT(*) FROM downvote_review_queue GROUP BY status; | ||||||
| ``` | ||||||
|
|
||||||
| Report findings to the user. If no unreviewed snippets exist, say so and stop. | ||||||
|
|
||||||
| ### 2. Group by theme | ||||||
|
|
||||||
| Analyze the snippet titles and categories to group them into thematic clusters. Present the groups to the user for review. | ||||||
|
|
||||||
| ### 3. Hide snippets | ||||||
|
|
||||||
| For any unhidden snippets, insert into user_hide_snippets: | ||||||
| ```sql | ||||||
| INSERT INTO user_hide_snippets (snippet) | ||||||
| VALUES ('<snippet_id>') | ||||||
| ON CONFLICT (snippet) DO NOTHING; | ||||||
| ``` | ||||||
|
|
||||||
| Also insert into the review queue: | ||||||
| ```sql | ||||||
| INSERT INTO downvote_review_queue (snippet_id, downvoted_at) | ||||||
| VALUES ('<snippet_id>', now()) | ||||||
| ON CONFLICT (snippet_id) DO NOTHING; | ||||||
| ``` | ||||||
|
|
||||||
| ### 4. Research and create KB entries | ||||||
|
|
||||||
| For each thematic group: | ||||||
| 1. Use subagents to research the correct facts via web search | ||||||
| 2. Find authoritative sources (Reuters, AP, NPR, official gov sites, fact-checkers) | ||||||
| 3. Check existing KB entries to avoid duplicates: | ||||||
| ```sql | ||||||
| SELECT id, fact FROM kb_entries WHERE status = 'active' AND fact ILIKE '%<keyword>%'; | ||||||
| ``` | ||||||
| 4. Insert new KB entries with sources using CTEs: | ||||||
| ```sql | ||||||
| WITH new_entry AS ( | ||||||
| INSERT INTO kb_entries (fact, related_claim, confidence_score, | ||||||
| disinformation_categories, keywords, is_time_sensitive, | ||||||
| valid_from, valid_until, created_by_model, status) | ||||||
| VALUES (...) RETURNING id | ||||||
| ) | ||||||
| INSERT INTO kb_entry_sources (kb_entry, url, source_name, source_type, | ||||||
| relevant_excerpt, access_date) | ||||||
| SELECT id, ..., CURRENT_DATE FROM new_entry; | ||||||
| ``` | ||||||
| 5. Use `created_by_model = 'claude-downvote-review'` for traceability | ||||||
|
|
||||||
| ### 5. Verify entries | ||||||
|
|
||||||
| Launch verification subagents to fact-check each new KB entry against live web sources. Fix any inaccuracies found. | ||||||
|
|
||||||
| ### 6. Generate embeddings | ||||||
|
|
||||||
| Run the backfill script: | ||||||
| ```bash | ||||||
| source .venv/bin/activate && python -m src.scripts.backfill_kb_embeddings | ||||||
| ``` | ||||||
|
|
||||||
| ### 7. Update queue status | ||||||
|
|
||||||
| Mark all processed snippets as completed: | ||||||
| ```sql | ||||||
| UPDATE downvote_review_queue | ||||||
| SET status = 'completed', processed_at = now(), kb_entries_created = <count> | ||||||
| WHERE snippet_id IN ('<id1>', '<id2>', ...); | ||||||
| ``` | ||||||
|
|
||||||
| ### 8. Generate report | ||||||
|
|
||||||
| Create a markdown report at `reports/<date>_downvote_review.md` with: | ||||||
| - Executive summary (snippets found, hidden, KB entries created) | ||||||
| - Grouped snippet analysis | ||||||
| - KB entries created with sources | ||||||
| - Verification results and corrections | ||||||
| - Database changes summary | ||||||
|
|
||||||
| Commit and push the report. | ||||||
|
|
||||||
| ### 9. Post to Slack | ||||||
|
|
||||||
| Post a summary to #verdad channel (ID: C07JYU3729G) using the Slack MCP tools, linking to the GitHub report. | ||||||
|
|
||||||
| ## Notes | ||||||
|
|
||||||
| - The VERDAD Supabase project ID is `dzujjhzgzguciwryzwlx` | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hardcoding the Supabase project ID here is a security and maintainability concern. While project IDs are not typically as sensitive as API keys, it's better practice to avoid hardcoding such values. If this command is executed in an environment where environment variables are available, it would be more secure to fetch this ID from an environment variable. This would also make it easier to point the skill to different environments (e.g., staging, production) without changing the command definition.
Suggested change
|
||||||
| - Valid source_type values: tier1_wire_service, tier1_factchecker, tier2_major_news, tier3_regional_news, official_source, other | ||||||
| - KB entries need confidence >= 70 and at least one external source | ||||||
| - Include Spanish-language keywords since the pipeline analyzes Spanish radio | ||||||
| - Always check existing KB entries before creating new ones to avoid duplicates | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| from .executor import Stage4Executor | ||
| from .flows import analysis_review | ||
| from .downvote_flows import downvote_review | ||
|
|
||
| __all__ = ["Stage4Executor", "analysis_review"] | ||
| __all__ = ["Stage4Executor", "analysis_review", "downvote_review"] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,111 @@ | ||||||||||||||||
| import asyncio | ||||||||||||||||
| import os | ||||||||||||||||
|
|
||||||||||||||||
| from prefect.task_runners import ConcurrentTaskRunner | ||||||||||||||||
|
|
||||||||||||||||
| from processing_pipeline.constants import PromptStage | ||||||||||||||||
| from processing_pipeline.stage_4.constants import Stage4SubStage | ||||||||||||||||
| from processing_pipeline.stage_4.tasks import ( | ||||||||||||||||
| fetch_a_specific_snippet_from_supabase, | ||||||||||||||||
| process_snippet, | ||||||||||||||||
| ) | ||||||||||||||||
| from processing_pipeline.supabase_utils import SupabaseClient | ||||||||||||||||
| from utils import optional_flow | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| @optional_flow( | ||||||||||||||||
| name="Stage 4: Downvote Review", | ||||||||||||||||
| log_prints=True, | ||||||||||||||||
| task_runner=ConcurrentTaskRunner, | ||||||||||||||||
| ) | ||||||||||||||||
| async def downvote_review(repeat=True): | ||||||||||||||||
| """Process downvoted snippets through the Stage 4 KB review pipeline. | ||||||||||||||||
|
|
||||||||||||||||
| Polls the downvote_review_queue table for pending entries. For each: | ||||||||||||||||
| 1. Claims the entry (atomic status update to prevent double-processing) | ||||||||||||||||
| 2. Ensures the snippet is hidden | ||||||||||||||||
| 3. Runs the full Stage 4 review pipeline (reviewer + KB researcher + | ||||||||||||||||
| web researcher + KB updater agents) | ||||||||||||||||
| 4. Marks the queue entry as completed or errored | ||||||||||||||||
| """ | ||||||||||||||||
| os.environ["GOOGLE_API_KEY"] = os.environ.get("GOOGLE_GEMINI_PAID_KEY") | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Modifying A safer approach would be to pass the API key explicitly to the client or function that requires it, rather than altering the environment. This would make the dependency clear and avoid unintended consequences. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard the Google key before writing to
Suggested fix- os.environ["GOOGLE_API_KEY"] = os.environ.get("GOOGLE_GEMINI_PAID_KEY")
+ google_api_key = os.getenv("GOOGLE_GEMINI_PAID_KEY")
+ if not google_api_key:
+ raise RuntimeError("GOOGLE_GEMINI_PAID_KEY is not set")
+ os.environ["GOOGLE_API_KEY"] = google_api_key📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
|
|
||||||||||||||||
| supabase_client = SupabaseClient( | ||||||||||||||||
| supabase_url=os.getenv("SUPABASE_URL"), | ||||||||||||||||
| supabase_key=os.getenv("SUPABASE_KEY"), | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| prompt_versions = { | ||||||||||||||||
| "kb_researcher": supabase_client.get_active_prompt( | ||||||||||||||||
| PromptStage.STAGE_4, Stage4SubStage.KB_RESEARCHER | ||||||||||||||||
| ), | ||||||||||||||||
| "web_researcher": supabase_client.get_active_prompt( | ||||||||||||||||
| PromptStage.STAGE_4, Stage4SubStage.WEB_RESEARCHER | ||||||||||||||||
| ), | ||||||||||||||||
| "reviewer": supabase_client.get_active_prompt( | ||||||||||||||||
| PromptStage.STAGE_4, Stage4SubStage.REVIEWER | ||||||||||||||||
| ), | ||||||||||||||||
| "kb_updater": supabase_client.get_active_prompt( | ||||||||||||||||
| PromptStage.STAGE_4, Stage4SubStage.KB_UPDATER | ||||||||||||||||
| ), | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| while True: | ||||||||||||||||
| pending = supabase_client.get_pending_downvote_reviews(limit=1) | ||||||||||||||||
| if not pending: | ||||||||||||||||
| if not repeat: | ||||||||||||||||
| print("No pending downvote reviews. Exiting.") | ||||||||||||||||
| break | ||||||||||||||||
| print("No pending downvote reviews. Sleeping 30s...") | ||||||||||||||||
| await asyncio.sleep(30) | ||||||||||||||||
| continue | ||||||||||||||||
|
|
||||||||||||||||
| queue_entry = pending[0] | ||||||||||||||||
| claimed = supabase_client.claim_downvote_review(queue_entry["id"]) | ||||||||||||||||
| if not claimed: | ||||||||||||||||
| print(f"Queue entry {queue_entry['id']} already claimed. Skipping.") | ||||||||||||||||
| continue | ||||||||||||||||
|
Comment on lines
+53
to
+67
|
||||||||||||||||
|
|
||||||||||||||||
| snippet_id = queue_entry["snippet_id"] | ||||||||||||||||
| print(f"Processing downvoted snippet: {snippet_id}") | ||||||||||||||||
|
|
||||||||||||||||
| snippet = fetch_a_specific_snippet_from_supabase(supabase_client, snippet_id) | ||||||||||||||||
| if not snippet: | ||||||||||||||||
| supabase_client.fail_downvote_review(queue_entry["id"], "Snippet not found") | ||||||||||||||||
| continue | ||||||||||||||||
|
|
||||||||||||||||
| try: | ||||||||||||||||
| supabase_client.hide_snippet_by_system(snippet_id) | ||||||||||||||||
|
|
||||||||||||||||
| # Prepend downvote context so the reviewer agents understand | ||||||||||||||||
| # this snippet was flagged as a false positive by users | ||||||||||||||||
| if snippet.get("context") and snippet["context"].get("main"): | ||||||||||||||||
| downvote_prefix = ( | ||||||||||||||||
| "[DOWNVOTE REVIEW CONTEXT] This snippet was downvoted by users " | ||||||||||||||||
| "as a FALSE POSITIVE — the content likely reports real events that " | ||||||||||||||||
| "were incorrectly flagged as disinformation. Focus on researching " | ||||||||||||||||
| "the correct facts and creating KB entries to prevent similar false " | ||||||||||||||||
| "positives in the future.\n\n" | ||||||||||||||||
| ) | ||||||||||||||||
| snippet["context"]["main"] = ( | ||||||||||||||||
| downvote_prefix + snippet["context"]["main"] | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| await process_snippet(supabase_client, snippet, prompt_versions) | ||||||||||||||||
| supabase_client.complete_downvote_review( | ||||||||||||||||
| queue_entry["id"], kb_entries_created=1 | ||||||||||||||||
|
Comment on lines
+94
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||||||||||||||||
| ) | ||||||||||||||||
|
Comment on lines
+95
to
+97
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The number of created knowledge base entries is hardcoded to To fix this, the
Suggested change
Comment on lines
+94
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't mark the queue item completed unconditionally.
Suggested direction- await process_snippet(supabase_client, snippet, prompt_versions)
- supabase_client.complete_downvote_review(
- queue_entry["id"], kb_entries_created=1
- )
+ result = await process_snippet(supabase_client, snippet, prompt_versions)
+ if not result["success"]:
+ supabase_client.fail_downvote_review(queue_entry["id"], result["error"])
+ continue
+ supabase_client.complete_downvote_review(
+ queue_entry["id"],
+ kb_entries_created=result["kb_entries_created"],
+ )This needs a matching change in 🤖 Prompt for AI Agents |
||||||||||||||||
| print(f"Downvote review completed for snippet {snippet_id}") | ||||||||||||||||
|
|
||||||||||||||||
| except Exception as e: | ||||||||||||||||
| error_msg = str(e) | ||||||||||||||||
| if isinstance(e, ExceptionGroup): | ||||||||||||||||
| error_msg = "\n".join( | ||||||||||||||||
| f"- {type(exc).__name__}: {exc}" for exc in e.exceptions | ||||||||||||||||
| ) | ||||||||||||||||
| print(f"Downvote review failed for snippet {snippet_id}: {error_msg}") | ||||||||||||||||
| supabase_client.fail_downvote_review(queue_entry["id"], error_msg) | ||||||||||||||||
|
|
||||||||||||||||
| if not repeat: | ||||||||||||||||
| break | ||||||||||||||||
| await asyncio.sleep(2) | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition
NOT EXISTS (SELECT 1 FROM user_hide_snippets ...)seems to contradict the new trigger's behavior. Theon_downvote_queue_reviewtrigger immediately hides a snippet upon a downvote by inserting it intouser_hide_snippets. Consequently, this query will not find any snippets that are downvoted after the trigger is active, potentially rendering this part of the on-demand skill ineffective for new downvotes.To ensure this command can process all relevant downvoted snippets, including those already hidden by the trigger, I recommend removing this
NOT EXISTSclause. The subsequent "Hide snippets" step already usesON CONFLICT DO NOTHING, which makes it safe to run even for snippets that are already hidden.