From b394349c9cfb6edd9fc32f8613483f33de3cb276 Mon Sep 17 00:00:00 2001 From: warlock20 Date: Sat, 25 Jul 2026 20:57:09 +0200 Subject: [PATCH] feat: Add counter-evidence generation feature to challenge research findings --- app/research_workflow/companion_routes.py | 28 ++++++++ .../prompts/companion/counter_evidence.yaml | 64 +++++++++++++++++++ app/services/argos/companion.py | 32 ++++++++++ 3 files changed, 124 insertions(+) create mode 100644 app/services/ai/prompts/companion/counter_evidence.yaml diff --git a/app/research_workflow/companion_routes.py b/app/research_workflow/companion_routes.py index ea53330a..edaea68a 100644 --- a/app/research_workflow/companion_routes.py +++ b/app/research_workflow/companion_routes.py @@ -111,6 +111,34 @@ def companion_ask(project_id): return json_error(str(e), status_code=500) +# ========================================================================= +# Counter-Evidence (Devil's Advocate) +# ========================================================================= + +@research_workflow_bp.route('/companion//counter-evidence', methods=['POST']) +@login_required +def companion_counter_evidence(project_id): + """Generate counter-evidence challenging a research finding.""" + project = get_user_resource_or_403(ResearchProject, project_id, current_user.id) + + data = request.json or {} + finding = data.get('finding', '').strip() + research_question = data.get('research_question', '').strip() or None + step_index = data.get('step_index', project.current_step_index) + + if not finding: + return json_validation_error('Finding is required') + + try: + argos = ArgosService(user_id=current_user.id) + context = argos.build_research_context(project_id, step_index=step_index) + counter_evidence = argos.generate_counter_evidence(context, finding, research_question) + return json_success('Counter-evidence generated', data={'counter_evidence': counter_evidence}) + except Exception as e: + logger.error(f"Counter-evidence generation failed: {e}") + return json_error(str(e), status_code=500) + + # ========================================================================= # Session Wrap-Up # ========================================================================= diff --git a/app/services/ai/prompts/companion/counter_evidence.yaml b/app/services/ai/prompts/companion/counter_evidence.yaml new file mode 100644 index 00000000..81a38b6a --- /dev/null +++ b/app/services/ai/prompts/companion/counter_evidence.yaml @@ -0,0 +1,64 @@ +name: "counter_evidence" +description: "Surface disconfirming evidence and questions that challenge a research finding" +version: "1.0" +category: "companion" + +preferred_provider: "gemini" +model: "gemini-3-pro-preview" +max_tokens: 6000 +temperature: 0.3 + +system_context: | + You are a devil's advocate for an investor's research finding. + Your role is to surface what would have to be TRUE for the finding to be WRONG. + + CRITICAL RULES: + 1. You surface DISCONFIRMING EVIDENCE, ALTERNATIVE EXPLANATIONS, and QUESTIONS. + You NEVER give investment opinions or recommendations. + 2. Never say "you should sell", "this is a bad investment", or "the thesis is wrong". + Say "this finding would not hold if..." and "the data that would disprove this is...". + 3. Distinguish clearly between what you know as fact and what is an untested assumption. + Label anything you cannot verify as "unverified". + 4. Attack the finding, never the investor. Reference their history as facts: + "Your mistake log shows a similar assumption on..." not "You tend to...". + 5. If the finding is already well-supported by the prior findings, say so plainly + rather than manufacturing weak objections. + +template: | + COMPANY: {company_name} + SECTOR: {sector_name} + CURRENT STEP: {step_name} + + FINDING TO CHALLENGE: + {finding} + + RESEARCH QUESTION IT ANSWERS: + {research_question} + + OTHER FINDINGS SO FAR: + {prior_findings} + + CURRENT THESIS: + {investment_thesis} + + KEY FLAGS: + Red flags: {red_flags} + Green flags: {green_flags} + + INVESTOR'S HISTORY: + Past mistakes: {mistake_summary} + Behavioral patterns to watch: {pattern_summary} + + Challenge the finding with: + 1. ASSUMPTIONS BEING MADE: What the finding takes for granted but has not verified + 2. DISCONFIRMING EVIDENCE TO LOOK FOR: Specific data, filings, or metrics that would + contradict the finding if they exist + 3. ALTERNATIVE EXPLANATIONS: Other readings of the same evidence + 4. QUESTIONS TO ANSWER NEXT: Concrete questions that would settle whether it holds + + Be specific and concrete. No opinions, no recommendations. Facts, gaps, and questions only. + +output_format: | + Structured text with the four numbered section headers above. + Two to four bullet points per section, one sentence each. + Every bullet should be checkable against a source the investor can go find. diff --git a/app/services/argos/companion.py b/app/services/argos/companion.py index 7d9c72af..a5857497 100644 --- a/app/services/argos/companion.py +++ b/app/services/argos/companion.py @@ -183,6 +183,38 @@ def ask_companion( logger.error(f"Companion chat failed: {e}") return f"Could not process question: {e}" + def generate_counter_evidence( + self, + context: CompanionContext, + finding: str, + research_question: str = None, + ) -> str: + """Play devil's advocate against a finding. Disconfirming evidence and questions, no opinions.""" + try: + prompt_data = prompt_service.get_prompt_with_metadata( + 'companion', 'counter_evidence', + company_name=context.company_name, + sector_name=context.sector_name, + step_name=context.step_name, + finding=finding, + research_question=research_question or context.research_questions, + prior_findings=context.prior_findings, + investment_thesis=context.investment_thesis, + red_flags=context.red_flags, + green_flags=context.green_flags, + mistake_summary=context.mistake_summary, + pattern_summary=context.pattern_summary, + ) + model_enum, provider_enum = resolve_model_provider( + prompt_data.get('metadata', {}), user_id=self.user_id, prompt_category='companion', + ) + return ai_service.generate_text( + prompt_data['prompt'], model=model_enum, provider=provider_enum, + ) + except Exception as e: + logger.error(f"Counter-evidence generation failed: {e}") + return f"Could not generate counter-evidence: {e}" + def wrap_up_session( self, context: CompanionContext,