Repository: fighthealthinsurance/fighthealthinsurance
Pull request #330: Improve pubmed more, increase logging, fix serializer issue
Author: login: holdenk
Created: 2025-03-19T19:23:21Z | Merged: 2025-03-20T05:53:14Z



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

- **New Features**
  - Users can now include a first name during account creation for more complete profile data.
  - A new method allows users to select PubMed articles to include in appeals.
  - Enhanced appeal question generation with additional context parameters for improved responses.
  - New fields added to the denial model for more detailed information and improved data handling.
  - Asynchronous capabilities introduced for PubMed article retrieval and processing.

- **Bug Fixes**
  - Improved error handling during the sending of appeal records to ensure visibility into issues.

- **Refactor**
  - Enhanced appeal processing, PDF generation, and article summarization through streamlined asynchronous operations and improved error handling, leading to a more responsive experience.
  - Updated data models to support more complex data types and additional manual inputs related to denials and appeals.

- **Chores**
  - Updated local user creation commands to incorporate the new first name parameter.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/fighthealthinsurance/common_view_logic.py b/fighthealthinsurance/common_view_logic.py
index 7f417d958..588525b31 100644
--- a/fighthealthinsurance/common_view_logic.py
+++ b/fighthealthinsurance/common_view_logic.py
@@ -13,6 +13,7 @@
 import os
 import uuid
 import re
+from stopit.utils import TimeoutException
 
 from django.core.files import File
 from django.core.validators import validate_email
@@ -276,11 +277,6 @@ def create_or_update_appeal(
             t.seek(0)
             doc_fname = os.path.basename(t.name)
 
-            # Create JSON representation of pubmed_ids if provided
-            pubmed_ids_json = None
-            if pubmed_ids_parsed:
-                pubmed_ids_json = json.dumps(pubmed_ids_parsed)
-
             if appeal is None:
                 appeal = Appeal.objects.create(
                     for_denial=denial,
@@ -291,7 +287,7 @@ def create_or_update_appeal(
                     creating_professional=creating_professional,
                     patient_user=patient_user,
                     domain=domain,
-                    pubmed_ids_json=pubmed_ids_json,
+                    pubmed_ids_json=pubmed_ids_parsed,
                 )
             else:
                 # Instead of using update(), set values individually preserving existing ones if not provided
@@ -310,8 +306,8 @@ def create_or_update_appeal(
                     appeal.patient_user = patient_user
                 if domain:
                     appeal.domain = domain
-                if pubmed_ids_json:
-                    appeal.pubmed_ids_json = pubmed_ids_json
+                if pubmed_ids_parsed:
+                    appeal.pubmed_ids_json = pubmed_ids_parsed
             if pending is not None:
                 appeal.pending = pending
             appeal.save()
@@ -401,9 +397,9 @@ def _assemble_appeal_pdf(
         if pubmed_ids_parsed is not None and len(pubmed_ids_parsed) > 0:
             logger.debug(f"Processing PubMed articles: {pubmed_ids_parsed}")
             pmt = PubMedTools()
-            pubmed_docs: list[PubMedArticleSummarized] = pmt.get_articles(
-                pubmed_ids_parsed
-            )
+            pubmed_docs: list[PubMedArticleSummarized] = async_to_sync(
+                pmt.get_articles
+            )(pubmed_ids_parsed)
             logger.debug(f"Retrieved {len(pubmed_docs)} PubMed articles")
             if pubmed_docs:
                 pubmed_docs_paths = [
@@ -895,6 +891,8 @@ async def generate_appeal_questions(cls, denial_id: int) -> List[Tuple[str, str]
         if not denial:
             logger.warning(f"Could not find denial with ID {denial_id}")
             return []
+        # For now this is disabled
+        return []
         try:
             # Check if we already have questions generated
             if denial.generated_questions is not None:
@@ -914,24 +912,32 @@ async def generate_appeal_questions(cls, denial_id: int) -> List[Tuple[str, str]
             patient_context = denial.health_history
             plan_context = denial.plan_context
 
-            # Use the ML model to generate questions with potential answers
-            raw_questions = await appealGenerator.get_appeal_questions(
-                denial_text=denial_text,
-                patient_context=patient_context,
-                plan_context=plan_context,
-            )
+            if denial.procedure:
 
-            # Ensure we're working with tuples
-            questions: List[Tuple[str, str]] = [
-                (q[0], q[1]) if isinstance(q, (list, tuple)) else (str(q), "")
-                for q in raw_questions
-            ]
+                # Use the ML model to generate questions with potential answers
+                raw_questions = await appealGenerator.get_appeal_questions(
+                    denial_text=denial_text,
+                    diagnosis=denial.diagnosis,
+                    procedure=denial.procedure,
+                    patient_context=patient_context,
+                    plan_context=plan_context,
+                )
 
-            # Store the questions in the generated_questions field
-            denial.generated_questions = questions
-            await denial.asave()
-            logger.debug(f"Generated {len(questions)} questions for denial {denial_id}")
-            return questions
+                # Ensure we're working with tuples
+                questions: List[Tuple[str, str]] = [
+                    (q[0], q[1]) if isinstance(q, (list, tuple)) else (str(q), "")
+                    for q in raw_questions
+                ]
+
+                # Store the questions in the generated_questions field
+                denial.generated_questions = questions
+                await denial.asave()
+                logger.debug(
+                    f"Generated {len(questions)} questions for denial {denial_id}"
+                )
+                return questions
+            else:
+                return []
         except Exception as e:
             logger.opt(exception=True).warning(
                 f"Failed to generate questions for denial {denial_id}: {e}"
@@ -1117,13 +1123,15 @@ async def just_the_name(task: Awaitable[tuple[str, Any]]) -> str:
             # Yield each result immediately for streaming
             yield result
 
-        # Now we see what optional tasks we can wrap up in the last 30 seconds.
+        # Now we see what optional tasks we can wrap up in the last 45 seconds.
         try:
-            for task in asyncio.as_completed(optional_tasks, timeout=30):
+            for task in asyncio.as_completed(optional_tasks, timeout=45):
                 result = await task
                 yield result
+        except asyncio.TimeoutError:
+            logger.debug("Ran out of time for optional tasks -- moving on")
         except Exception as e:
-            logger.opt(exception=True).debug(f"Error processing optional tasks: {e}")
+            logger.debug(f"Error processing optional tasks: {e}")
             yield f"Error processing optional tasks: {str(e)}\n"
 
         yield "Extraction completed\n"
@@ -1154,7 +1162,7 @@ async def find_pubmed_articles():
                     try:
                         pubmed_tool = PubMedTools()
                         # Find related articles based on diagnosis and procedure
-                        pubmed_tool.find_pubmed_articles_for_denial(
+                        await pubmed_tool.find_pubmed_articles_for_denial(
                             denial, timeout=60.0
                         )
                     except Exception as e:
@@ -1359,7 +1367,9 @@ def format_denial_response_info(cls, denial):
         if Appeal.objects.filter(for_denial=denial).exists():
             appeal_id = Appeal.objects.get(for_denial=denial).id
         else:
-            logger.debug("Could not find appeal for {denial}")
+            logger.debug(
+                f"Could not find appeal for {denial} -- expected for consumer version"
+            )
         r = DenialResponseInfo(
             selected_denial_type=denial.denial_type.all(),
             all_denial_types=cls.all_denial_types(),
@@ -1401,9 +1411,19 @@ async def generate_appeals(cls, parameters) -> AsyncIterator[str]:
 
         # Get the current info
         await asyncio.sleep(0)
-        denial = await Denial.objects.filter(
-            denial_id=denial_id, semi_sekret=semi_sekret, hashed_email=hashed_email
-        ).aget()
+        denial = (
+            await Denial.objects.filter(
+                denial_id=denial_id, semi_sekret=semi_sekret, hashed_email=hashed_email
+            )
+            .select_related(
+                "patient_user",
+                "patient_user__user",
+                "domain",
+                "primary_professional",
+                "primary_professional__user",
+            )
+            .aget()
+        )
 
         non_ai_appeals: List[str] = list(
             map(
@@ -1490,11 +1510,17 @@ async def generate_appeals(cls, parameters) -> AsyncIterator[str]:
         pubmed_context = None
         logger.debug("Looking up the pubmed context")
         try:
-            pubmed_context = await sync_to_async(cls.pmt.find_context_for_denial)(
-                denial
+            pubmed_context = asyncio.wait_for(
+                cls.pmt.find_context_for_denial(denial), timeout=60
+            )
+        except TimeoutException as t:
+            logger.opt(exception=True).debug(
+                f"Timeout {t} looking up context for {denial}."
             )
         except Exception as e:
-            logger.debug(f"Error {e} looking up context for {denial}.")
+            logger.opt(exception=True).debug(
+                f"Error {e} looking up context for {denial}."
+            )
         logger.debug("Pubmed context done.")
 
         try:
@@ -1546,18 +1572,17 @@ async def sub_in_appeals(appeal: dict[str, str]) -> dict[str, str]:
                 "{diagnosis}": denial.diagnosis or "{diagnosis}",
                 "{procedure}": denial.procedure or "{procedure}",
             }
-            # TODO: Update for async
-            # if denial.patient_user is not None:
-            #    subs["[Patient Name]"] = denial.patient_user.get_legal_name()
-            # if denial and denial.primary_professional is not None:
-            #    subs["[Professional Name]"] = (
-            #        denial.primary_professional.get_full_name()
-            #    )
-            # if denial.domain:
-            #    subs["[Professional Address]"] = denial.domain.get_address()
+            if denial.patient_user is not None:
+                subs["[Patient Name]"] = denial.patient_user.get_legal_name()
+            if denial and denial.primary_professional is not None:
+                subs["[Professional Name]"] = (
+                    denial.primary_professional.get_full_name()
+                )
+            if denial.domain:
+                subs["[Professional Address]"] = denial.domain.get_address()
             ret = s.safe_substitute(subs)
             appeal["content"] = ret
-            logger.debug("Updated appeal to {ret}")
+            logger.debug(f"Updated appeal to {ret}")
             return appeal
 
         async def format_response(response: dict[str, str]) -> str:
diff --git a/fighthealthinsurance/forms/__init__.py b/fighthealthinsurance/forms/__init__.py
index 7a39aaa43..a65b432df 100644
--- a/fighthealthinsurance/forms/__init__.py
+++ b/fighthealthinsurance/forms/__init__.py
@@ -85,6 +85,7 @@ class ProDenialForm(BaseDenialForm):
     patient_id = forms.CharField(required=False)
     insurance_company = forms.CharField(required=False)
     patient_visible = forms.BooleanField(required=False)
+    denial_id = forms.IntegerField(required=False)
 
 
 class DenialRefForm(forms.Form):
diff --git a/fighthealthinsurance/generate_appeal.py b/fighthealthinsurance/generate_appeal.py
index 068cbdc02..92351e1be 100755
--- a/fighthealthinsurance/generate_appeal.py
+++ b/fighthealthinsurance/generate_appeal.py
@@ -48,6 +48,8 @@ def __init__(self):
     async def get_appeal_questions(
         self,
         denial_text: str,
+        procedure: Optional[str],
+        diagnosis: Optional[str],
         patient_context: Optional[str] = None,
         plan_context: Optional[str] = None,
         use_external: bool = False,
@@ -73,6 +75,8 @@ async def get_appeal_questions(
                 try:
                     raw_questions = await model.get_appeal_questions(
                         denial_text=denial_text,
+                        procedure=procedure,
+                        diagnosis=diagnosis,
                         patient_context=patient_context,
                         plan_context=plan_context,
                     )
@@ -90,6 +94,8 @@ async def get_appeal_questions(
             try:
                 raw_questions = await model.get_appeal_questions(
                     denial_text=denial_text,
+                    procedure=procedure,
+                    diagnosis=diagnosis,
                     plan_context=plan_context,
                 )
                 if raw_questions and len(raw_questions) > 0:
diff --git a/fighthealthinsurance/management/commands/make_user.py b/fighthealthinsurance/management/commands/make_user.py
index d18589d14..cdf09e550 100644
--- a/fighthealthinsurance/management/commands/make_user.py
+++ b/fighthealthinsurance/management/commands/make_user.py
@@ -27,6 +27,9 @@ def add_arguments(self, parser: CommandParser) -> None:
         parser.add_argument(
             "--email", required=True, help="User's valid email address."
         )
+        parser.add_argument(
+            "--first-name", required=False, help="User's first name", default="unknown"
+        )
         parser.add_argument(
             "--password", required=True, help="User's password (minimum 8 characters)."
         )
@@ -55,6 +58,7 @@ def handle(self, *args: str, **options: Any) -> None:
         email = options["email"].strip()
         password = options["password"]
         domain_input = options["domain"]
+        first_name = options.get("first_name", "test_first_name")
         visible_phone_number = options.get("visible_phone_number", "0")
         is_provider = options.get("is_provider", True)
 
@@ -99,6 +103,7 @@ def handle(self, *args: str, **options: Any) -> None:
                     username=combined_username,
                     email=email,
                     password=password,
+                    first_name=first_name,
                 )
                 if hasattr(user, "is_provider"):
                     user.is_provider = is_provider
diff --git a/fighthealthinsurance/migrations/0091_denial_flag_for_exclude_and_more.py b/fighthealthinsurance/migrations/0091_denial_flag_for_exclude_and_more.py
new file mode 100644
index 000000000..c8a235289
--- /dev/null
+++ b/fighthealthinsurance/migrations/0091_denial_flag_for_exclude_and_more.py
@@ -0,0 +1,48 @@
+# Generated by Django 5.1.5 on 2025-03-20 00:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("fighthealthinsurance", "0090_denial_generated_questions"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="denial",
+            name="flag_for_exclude",
+            field=models.BooleanField(default=False, null=True),
+        ),
+        migrations.AddField(
+            model_name="denial",
+            name="manual_deidentified_appeal",
+            field=models.TextField(default="", null=True),
+        ),
+        migrations.AddField(
+            model_name="denial",
+            name="manual_deidentified_denial",
+            field=models.TextField(default="", null=True),
+        ),
+        migrations.AddField(
+            model_name="denial",
+            name="manual_searchterm",
+            field=models.TextField(default="", null=True),
+        ),
+        migrations.AddField(
+            model_name="denial",
+            name="verified_diagnosis",
+            field=models.TextField(default="", null=True),
+        ),
+        migrations.AddField(
+            model_name="denial",
+            name="verified_procedure",
+            field=models.TextField(default="", null=True),
+        ),
+        migrations.AlterField(
+            model_name="denial",
+            name="pubmed_ids_json",
+            field=models.JSONField(blank=True, null=True),
+        ),
+    ]
diff --git a/fighthealthinsurance/migrations/0092_alter_appeal_pubmed_ids_json_alter_faxestosend_pmids.py b/fighthealthinsurance/migrations/0092_alter_appeal_pubmed_ids_json_alter_faxestosend_pmids.py
new file mode 100644
index 000000000..b2f3bbd6a
--- /dev/null
+++ b/fighthealthinsurance/migrations/0092_alter_appeal_pubmed_ids_json_alter_faxestosend_pmids.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.1.5 on 2025-03-20 00:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("fighthealthinsurance", "0091_denial_flag_for_exclude_and_more"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="appeal",
+            name="pubmed_ids_json",
+            field=models.JSONField(blank=True, null=True),
+        ),
+        migrations.AlterField(
+            model_name="faxestosend",
+            name="pmids",
+            field=models.JSONField(blank=True, null=True),
+        ),
+    ]
diff --git a/fighthealthinsurance/ml/ml_models.py b/fighthealthinsurance/ml/ml_models.py
index f3c701a8b..33141ed30 100644
--- a/fighthealthinsurance/ml/ml_models.py
+++ b/fighthealthinsurance/ml/ml_models.py
@@ -17,6 +17,8 @@
 from fighthealthinsurance.utils import all_concrete_subclasses
 from fighthealthinsurance.process_denial import DenialBase
 
+from stopit import TimeoutException
+
 
 class RemoteModelLike(DenialBase):
     def infer(
@@ -71,6 +73,8 @@ async def get_procedure_and_diagnosis(
     async def get_appeal_questions(
         self,
         denial_text: str,
+        procedure: Optional[str],
+        diagnosis: Optional[str],
         patient_context: Optional[str] = None,
         plan_context: Optional[str] = None,
     ) -> List[Tuple[str, str]]:
@@ -244,6 +248,11 @@ def bad_result(self, result: Optional[str], infer_type: str) -> bool:
         generic_bad_ideas = [
             "Therefore, the Health Plans denial should be overturned.",
             "llama llama virus",
+            "The independent medical review found that",
+            "The independent review findings for",
+            "the physician reviewer overturned",
+            "91111111111111111111111",
+            "I need the text to be able to help you with your appeal",
         ]
         if result is None:
             return True
@@ -549,8 +558,12 @@ async def __timeout_infer(
         **kwargs,
     ) -> Optional[str]:
         if self._timeout is not None:
-            with Timeout(self._timeout) as timeout_ctx:
-                return await self.__infer(*args, **kwargs)
+            try:
+                with Timeout(self._timeout) as timeout_ctx:
+                    return await self.__infer(*args, **kwargs)
+            except TimeoutException as e:
+                logger.debug(f"Timed out querying {self}")
+                return None
         else:
             return await self.__infer(*args, **kwargs)
 
@@ -656,15 +669,29 @@ def __init__(
                 """You must be concise. You have an in-depth understanding of insurance and have gained extensive experience working in a medical office. Your expertise lies in deciphering health insurance denial letters to identify the requested procedure and, if available, the associated diagnosis. Each word costs an extra dollar. Provide a concise response with the procedure on one line starting with "Procedure" and Diagnsosis on another line starting with Diagnosis. Do not say not specified. Diagnosis can also be reason for treatment even if it's not a disease (like high risk homosexual behaviour for prep or preventitive and the name of the diagnosis). Remember each result on a seperated line."""
             ],
             "questions": [
-                """You have an in-depth understanding of insurance and have gained extensive experience working in a medical office. Generate 5-10 specific, detailed questions that would help craft a more effective appeal for a health insurance denial. Focus on questions that would gather information about:
-1. Medical necessity of the procedure/treatment
-2. Prior treatments tried and their outcomes
-3. Supporting medical evidence or studies
-4. Details about the denial reason
-5. Patient's specific situation related to the diagnosis
-6. Impact on the patient's health without this treatment
-7. Relevant insurance policy details
-Keep each question on a separate line and make them directly applicable to the specific denial case."""
+                """You have deep expertise in health insurance and extensive experience working in a medical office. Your task is to generate the best one to four specific, detailed questions that will help craft a stronger appeal for a health insurance denial.
+
+### Key Guidelines:
+- No Independent Medical Review (IMR/IME) has occured yet We are only dealing with the insurance company at this stage, so do not reference independent medical reviews.
+- Patient/Medical Assistant-Friendly: The questions will likely be answered by a patient or a medical assistant, so avoid technical jargon.
+- Focus on Establishing Validity: Do not ask about the insurance company’s stated reason for denial. Instead, ask patient-related questions that help demonstrate why the denial is invalid.
+### Question Format Preferences:
+  - Yes/no questions are ideal.
+  - Short-answer questions (one sentence max) are acceptable.
+  - Avoid long-answer questions, as they may overwhelm the patient.
+### Bad questions (do not ask):
+  - Why was the treatment considered not medically necessary?
+  - What specific criteria were used to determine medical necessity for this treatment?
+  - What is the insurance company’s stated reason for denial?
+  - Can you provide more information on the clinical evidence and guidelines that support the medical necessity of this treatment?
+### Good Examples:
+  - Wegovy denial: "Has the patient participated in a structured weight loss program (e.g., Weight Watchers)?"
+  - PrEP denial: "How many sexual partners (roughly) has the patient had in the past 12 months?"
+  - Mammogram denial: "What is the patient’s age?" "Does the patient have the BRCA1 mutation or a family history of breast cancer?"
+### Context-Aware Answers:
+ -If a question has a likely answer from the provided context, include the answer on the same line after the question mark.
+- Case-Specific: Each question should be directly relevant to the specific denial reason and medical necessity at hand.
+"""
             ],
             "medically_necessary": [
                 """You have an in-depth understanding of insurance and have gained extensive experience working in a medical office. Your expertise lies in deciphering health insurance denial letters. Each word costs an extra dollar. Please provide a concise response. Do not use the 3rd person when referring to the patient (e.g. don't say "the patient", "patient's", "his", "hers"), instead use the first person (I, my, mine,etc.) when talking about the patient. You are not a review and should not mention any. Write concisely in a professional tone akin to patio11. Do not say this is why the decission should be overturned. Just say why you believe it is medically necessary (e.g. to prevent X or to treat Y)."""
@@ -685,6 +712,8 @@ def __init__(
     async def get_appeal_questions(
         self,
         denial_text: str,
+        procedure: Optional[str],
+        diagnosis: Optional[str],
         patient_context: Optional[str] = None,
         plan_context: Optional[str] = None,
     ) -> List[Tuple[str, str]]:
@@ -695,12 +724,34 @@ async def get_appeal_questions(
         Args:
             denial_text: The text of the denial letter
             patient_context: Optional patient health history or context
+            procedure: the procedure
+            diagnosis: primary diagnosis
             plan_context: Optional insurance plan context
 
         Returns:
             A list of tuples (question, answer) where answer may be empty
         """
-        prompt = f"Based on this denial letter, generate specific questions that would help create a more effective appeal:\n\n{denial_text}"
+        procedure_opt = (
+            "The procedure denied was {procedure} so only ask questions relevant to {procedure}"
+            if procedure
+            else ""
+        )
+        patient_context_opt = (
+            "Optional patient context: {patient_context}" if patient_context else ""
+        )
+        diagnosis_opt = "The primary diagnosis was {diagnosis}" if diagnosis else ""
+        # Procedure opt is in their multiple times intentionally. ~computers~
+        prompt = f"""
+        Some context which can help you in your task: {denial_text}
+        {procedure_opt} \n
+        {patient_context_opt} \n
+        {diagnosis_opt} \n
+        {procedure_opt} \n
+        Your task is to write one to three patient friendly questions about the patient history to help appeal this denial. \n
+        Remember to keep the questions concise and patient-friendly and focused on the potential patient history.\n
+        If you ask questions about the denial it's self the patient will be sad and give up so don't do that.
+        {procedure_opt} \n
+        """
 
         system_prompts: list[str] = self.get_system_prompts("questions")
 
@@ -890,7 +941,7 @@ class DeepInfra(RemoteFullOpenLike):
     """Use DeepInfra."""
 
     def __init__(self, model: str):
-        api_base = "https://api.deepinfra.com/v1/openai/chat/completions"
+        api_base = "https://api.deepinfra.com/v1/openai"
         token = os.getenv("DEEPINFRA_API")
         if token is None or len(token) < 1:
             raise Exception("No token found for deepinfra")
@@ -899,6 +950,11 @@ def __init__(self, model: str):
     @classmethod
     def models(cls) -> List[ModelDescription]:
         return [
+            ModelDescription(
+                cost=20,
+                name="google/gemma-3-27b-it",
+                internal_name="google/gemma-3-27b-it",
+            ),
             ModelDescription(
                 cost=179,
                 name="meta-llama/meta-llama-3.1-405B-instruct",
diff --git a/fighthealthinsurance/ml/ml_router.py b/fighthealthinsurance/ml/ml_router.py
index 9e13d4d8a..17282de71 100644
--- a/fighthealthinsurance/ml/ml_router.py
+++ b/fighthealthinsurance/ml/ml_router.py
@@ -68,27 +68,41 @@ def entity_extract_backends(self, use_external) -> list[RemoteModelLike]:
         else:
             return self.internal_models_by_cost
 
-    def summarize(self, text: Optional[str], abstract: Optional[str]) -> Optional[str]:
+    async def summarize(
+        self, title: Optional[str], text: Optional[str], abstract: Optional[str] = None
+    ) -> Optional[str]:
         models: list[RemoteModelLike] = []
-        if "meta-llama/Llama-3.3-70B-Instruct-Turbo" in self.models_by_name:
-            models = self.models_by_name["meta-llama/Llama-3.3-70B-Instruct-Turbo"]
+        if "google/gemma-3-27b-it" in self.models_by_name:
+            models = (
+                self.models_by_name["google/gemma-3-27b-it"]
+                + self.internal_models_by_cost
+            )
         else:
-            models = self.all_models_by_cost
+            models = self.internal_models_by_cost
         abstract_optional = ""
         text_optional = ""
         if abstract is not None:
-            abstract_optional = f"--- Current abstract: {abstract} ---"
+            abstract_optional = f"--- Current abstract: {abstract[0:1000]} ---"
         if text is not None:
-            text_optional = f"--- Full-ish article text: {text} ---"
+            text_optional = f"--- Full-ish article text: {text[0:1000]} ---"
         for m in models:
-            return asyncio.run(
-                m._infer(
-                    system_prompts=[
-                        "You are a helpful assistant summarizing an article for a person or other LLM wriitng an appeal. Be very concise."
-                    ],
-                    prompt=f"Summarize the following for use in a health insurance appeal: {abstract_optional} {text_optional}.",
-                )
+            r = await m._infer(
+                system_prompts=[
+                    "You are a helpful assistant summarizing article(s) for a person or other LLM wriitng an appeal. Be very concise."
+                ],
+                prompt=f"Summarize the following {title} for use in a health insurance appeal: {abstract_optional}{text_optional}. If present in the input include a list of the most relevant articles referenced (with PMID / DOIs or links if present in the input). If multile studies prefer US studies then generic non-country specific and then other countries. We're focused on helping american patients and providers.",
+            )
+            if r is not None:
+                return r
+            # Try again with only the abstract
+            r = await m._infer(
+                system_prompts=[
+                    "You are a helpful assistant summarizing article(s) for a person or other LLM wriitng an appeal. Be very concise."
+                ],
+                prompt=f"Summarize the following {title} for use in a health insurance appeal: {abstract_optional}. If present in the input include a list of the most relevant articles referenced (with PMID / DOIs or links if present in the input). If multile studies prefer US studies then generic non-country specific and then other countries. We're focused on helping american patients and providers.",
             )
+            if r is not None:
+                return r
         return None
 
     def working(self) -> bool:
diff --git a/fighthealthinsurance/models.py b/fighthealthinsurance/models.py
index f1fcc3620..42e4bbd2e 100755
--- a/fighthealthinsurance/models.py
+++ b/fighthealthinsurance/models.py
@@ -317,7 +317,7 @@ class FaxesToSend(ExportModelOperationsMixin("FaxesToSend"), models.Model):  # t
     email = models.CharField(max_length=300)
     name = models.CharField(max_length=300, null=True)
     appeal_text = models.TextField()
-    pmids = models.CharField(max_length=600, blank=True, null=True)
+    pmids = models.JSONField(null=True, blank=True)
     health_history = models.TextField(null=True, blank=True)
     combined_document = models.FileField(null=True, storage=settings.COMBINED_STORAGE)
     combined_document_enc = EncryptedFileField(
@@ -433,8 +433,18 @@ class Denial(ExportModelOperationsMixin("Denial"), models.Model):  # type: ignor
     health_history_anonymized = models.BooleanField(default=True)
     single_case = models.BooleanField(default=False, null=True)
     # pubmed articles to be used to create the input context to the appeal
-    pubmed_ids_json = models.CharField(max_length=600, blank=True)
+    pubmed_ids_json = models.JSONField(null=True, blank=True)
     generated_questions = models.JSONField(null=True, blank=True)
+    manual_deidentified_denial = models.TextField(
+        primary_key=False, null=True, default=""
+    )
+    manual_deidentified_appeal = models.TextField(
+        primary_key=False, null=True, default=""
+    )
+    manual_searchterm = models.TextField(primary_key=False, null=True, default="")
+    verified_procedure = models.TextField(primary_key=False, null=True, default="")
+    verified_diagnosis = models.TextField(primary_key=False, null=True, default="")
+    flag_for_exclude = models.BooleanField(default=False, null=True)
 
     @classmethod
     def filter_to_allowed_denials(cls, current_user: User):
@@ -556,7 +566,7 @@ class Appeal(ExportModelOperationsMixin("Appeal"), models.Model):  # type: ignor
     patient_send = models.BooleanField(default=True)
     patient_visible = models.BooleanField(default=True)
     # Pubmed IDs for the articles to be included in the appeal
-    pubmed_ids_json = models.CharField(max_length=600, blank=True, null=True)
+    pubmed_ids_json = models.JSONField(blank=True, null=True)
     response_document_enc = EncryptedFileField(
         null=True, storage=settings.COMBINED_STORAGE
     )
diff --git a/fighthealthinsurance/pubmed_tools.py b/fighthealthinsurance/pubmed_tools.py
index 8b2ffb19b..79f600c20 100644
--- a/fighthealthinsurance/pubmed_tools.py
+++ b/fighthealthinsurance/pubmed_tools.py
@@ -3,15 +3,18 @@
     PubMedQueryData,
     PubMedMiniArticle,
 )
+from asgiref.sync import sync_to_async, async_to_sync
 from fighthealthinsurance.utils import pubmed_fetcher
 from .utils import markdown_escape
 from concurrent.futures import Future
 from metapub import FindIt
 from stopit import ThreadingTimeout as Timeout
+from stopit import TimeoutException
 from .models import Denial
 import json
 import PyPDF2
 import requests
+import aiohttp
 from fighthealthinsurance.ml.ml_router import ml_router
 import tempfile
 from typing import List, Optional, Dict, Tuple, Any, Set
@@ -20,17 +23,21 @@
 from loguru import logger
 import eutils
 from datetime import datetime, timedelta
+import asyncio
+
+
+PER_QUERY = 3
 
 
 class PubMedTools(object):
     # Rough bias to "recent" articles
-    since_list = ["2025", "2024", None]
+    since_list = ["2024", None]
 
-    def find_pubmed_article_ids_for_query(
+    async def find_pubmed_article_ids_for_query(
         self,
         query: str,
         since: Optional[str] = None,
-        timeout: float = 30.0,
+        timeout: float = 60.0,
     ) -> List[str]:
         """
         Find PubMed article IDs for a given query. May pull from database.
@@ -54,9 +61,9 @@ def find_pubmed_article_ids_for_query(
                     since=since,
                 ).order_by("-created")
 
-                if existing_queries.exists():
+                if await existing_queries.aexists():
                     # Use cached query results
-                    for query_data in existing_queries:
+                    async for query_data in existing_queries:
                         if query_data.articles:
                             try:
                                 article_ids: list[str] = json.loads(
@@ -70,49 +77,30 @@ def find_pubmed_article_ids_for_query(
                                 )
 
                 # If no cache or cache error, fetch from PubMed API
-                fetched_pmids = pubmed_fetcher.pmids_for_query(query, since=since)
+                logger.debug(f"Querying pubmed for query {query}")
+                fetched_pmids = await sync_to_async(pubmed_fetcher.pmids_for_query)(
+                    query, since=since
+                )
+                logger.debug(f"Got back initial pmids {fetched_pmids}")
                 if fetched_pmids:
                     # Sometimes we get nulls...
                     articles_json = json.dumps(fetched_pmids).replace("\x00", "")
-                    PubMedQueryData.objects.create(
+                    await PubMedQueryData.objects.acreate(
                         query=query,
                         since=since,
                         articles=articles_json,
-                    ).save()
-                    pmids.extend(fetched_pmids)
-
-                # If we didn't get any results, but we're not restricted by 'since',
-                # try querying with each since value in our since_list until we find results
-                if not pmids and since is None:
-                    for year in self.since_list:
-                        if year is not None:
-                            # Don't re-try with None since we already did that
-                            logger.debug(
-                                f"No results for {query}, trying with since={year}"
-                            )
-                            year_pmids = pubmed_fetcher.pmids_for_query(
-                                query, since=year
-                            )
-                            if year_pmids:
-                                # Cache the results
-                                articles_json = json.dumps(year_pmids).replace(
-                                    "\x00", ""
-                                )
-                                PubMedQueryData.objects.create(
-                                    query=query,
-                                    since=year,
-                                    articles=articles_json,
-                                ).save()
-                                pmids.extend(year_pmids)
-                                break
+                    )
+                    return fetched_pmids
         except Exception as e:
             # We might timeout
-            logger.debug(f"Error or timeout in find_pubmed_article_ids_for_query: {e}")
+            logger.opt(exception=True).debug(
+                f"Error or timeout in find_pubmed_article_ids_for_query: {e}"
+            )
             pass
         return pmids
 
-    def find_pubmed_articles_for_denial(
-        self, denial: Denial, timeout=30.0
+    async def find_pubmed_articles_for_denial(
+        self, denial: Denial, timeout=70.0
     ) -> List[PubMedMiniArticle]:
         pmids: List[str] = []
         articles: List[PubMedMiniArticle] = []
@@ -131,7 +119,7 @@ def find_pubmed_articles_for_denial(
                         count = 0
                         if query is None or query.strip() == "":
                             continue
-                        new_pmids = self.find_pubmed_article_ids_for_query(
+                        new_pmids = await self.find_pubmed_article_ids_for_query(
                             query, since=since
                         )
                         for pmid in new_pmids:
@@ -139,23 +127,35 @@ def find_pubmed_articles_for_denial(
                                 count = count + 1
                                 unique_pmids.add(pmid)
                                 pmids.append(pmid)
-                                # Add 5 unique per query set
-                                if count >= 5:
+                                if count >= PER_QUERY:
                                     break
 
                 # Check if articles already exist in database
                 for pmid in pmids:
-                    mini_article = PubMedMiniArticle.objects.filter(pmid=pmid).first()
+                    mini_article = await PubMedMiniArticle.objects.filter(
+                        pmid=pmid
+                    ).afirst()
                     if mini_article:
                         articles.append(mini_article)
                     else:
                         # Create a new mini article
                         try:
-                            fetched = pubmed_fetcher.article_by_pmid(pmid)
+                            fetched = await sync_to_async(
+                                pubmed_fetcher.article_by_pmid
+                            )(pmid)
                             if fetched:
-                                src = FindIt(pmid)
-                                url = src.url
-                                mini_article = PubMedMiniArticle.objects.create(
+                                url = None
+                                try:
+                                    logger.debug(f"Looking for {pmid} with findit")
+                                    src = asyncio.wait_for(
+                                        sync_to_async(FindIt)(pmid),
+                                        timeout=timeout / 5.0,
+                                    )
+                                    logger.debug(f"Found it {src}")
+                                    url = src.url
+                                except Exception:
+                                    logger.debug("Findit failed.")
+                                mini_article = await PubMedMiniArticle.objects.acreate(
                                     pmid=pmid,
                                     title=(
                                         fetched.title.replace("\x00", "")
@@ -171,14 +171,20 @@ def find_pubmed_articles_for_denial(
                                 )
                                 articles.append(mini_article)
                         except Exception as e:
-                            logger.error(f"Error fetching article {pmid}: {e}")
-        except Timeout as e:
+                            logger.opt(exception=True).debug(
+                                f"Error fetching article {pmid}: {e}"
+                            )
+        except TimeoutException as e:
             logger.debug(
                 f"Timeout in find_pubmed_articles_for_denial: {e} so far got {articles}"
             )
+        except Exception as e:
+            logger.opt(exception=True).debug(f"Unexpected error {e}")
+            raise e
+        logger.debug(f"Found {articles}")
         return articles
 
-    def find_context_for_denial(self, denial: Denial, timeout=30.0) -> str:
+    async def find_context_for_denial(self, denial: Denial, timeout=60.0) -> str:
         """
         Kind of hacky RAG routine that uses PubMed.
         """
@@ -192,9 +198,9 @@ def find_context_for_denial(self, denial: Denial, timeout=30.0) -> str:
             with Timeout(timeout) as _timeout_ctx:
                 # Check if the denial has specific pubmed IDs selected already
                 selected_pmids: Optional[list[str]] = None
-                if denial.pubmed_ids_json and denial.pubmed_ids_json.strip():
+                if denial.pubmed_ids_json and len(denial.pubmed_ids_json) > 0:
                     try:
-                        selected_pmids = json.loads(denial.pubmed_ids_json)
+                        selected_pmids = denial.pubmed_ids_json
                         if selected_pmids:  # Check if not None and not empty
                             logger.info(
                                 f"Using {len(selected_pmids)} pre-selected PubMed articles for denial {denial.denial_id}"
@@ -211,54 +217,87 @@ def find_context_for_denial(self, denial: Denial, timeout=30.0) -> str:
                     if not query or query.strip() == "":
                         return ""  # Return empty string if no query available
 
-                    selected_pmids = list(
-                        map(
-                            lambda x: x.pmid,
-                            self.find_pubmed_articles_for_denial(
-                                denial, timeout=(timeout / 2.0)
-                            ),
-                        )
+                    possible_articles = asyncio.wait_for(
+                        self.find_pubmed_articles_for_denial(
+                            denial, timeout=(timeout / 2.5)
+                        ),
+                        timeout=timeout / 2.0,
                     )
 
+                    selected_pmids = list(map(lambda x: x.pmid, possible_articles))
+
+                denial.pubmed_ids_json = selected_pmids
+                logger.debug(f"Updating denial to have some context selected...")
+                await denial.asave()
                 # Directly fetch the selected articles from the database
-                articles = list(
-                    PubMedArticleSummarized.objects.filter(pmid__in=selected_pmids)
-                )
+                articles = [
+                    article
+                    async for article in PubMedArticleSummarized.objects.filter(
+                        pmid__in=selected_pmids
+                    )
+                ]
                 logger.debug(
                     f"Found {len(articles)} pre-selected articles in the database"
                 )
 
                 # If we couldn't find all the articles in the database, try to fetch them
-                missing_pmids = []
                 if articles and len(articles) < len(selected_pmids):
                     for pmid in selected_pmids:
                         if not any(a.pmid == pmid for a in articles):
                             missing_pmids.append(pmid)
+                elif articles and len(articles) == len(selected_pmids):
+                    missing_pmids = []
                 else:
                     missing_pmids = selected_pmids
 
                 if missing_pmids and len(missing_pmids) > 0:
                     logger.debug(f"Fetching {len(missing_pmids)} missing articles")
-                # Fetch in-order so we can be interrupted
-                for pmid in missing_pmids:
-                    articles.extend(self.get_articles([pmid]))
-        except Timeout as e:
+                    # Fetch in-order so we can be interrupted
+                    for pmid in missing_pmids:
+                        articles.extend(await self.get_articles([pmid]))
+        except TimeoutException as e:
             logger.debug(
                 f"Timeout in find_context_for_denial: {e} so far got {articles}"
             )
+        except Exception as e:
+            logger.opt(exception=True).debug("Non-timeout error -- {e}?")
+            raise e
 
         # Format the articles for context
         if articles:
-            return "\n".join(self.format_article_short(article) for article in articles)
+            logger.debug("Making single context input")
+            joined_contexts = "\n".join(
+                self.format_article_short(article) for article in articles
+            )
+            if len(joined_contexts) < 100:
+                logger.debug("Not much input, skpping summary step.")
+                return joined_contexts
+            r = await ml_router.summarize(
+                title="Combined contexts", text=joined_contexts
+            )
+            logger.debug("Huzzah!")
+            if r is None:
+                return joined_contexts
+            else:
+                return r
         else:
             return ""
 
     @staticmethod
     def format_article_short(article: PubMedArticleSummarized) -> str:
         """Helper function to format an article for context."""
-        return f"PubMed DOI {article.doi} title {article.title} summary {article.basic_summary}"
-
-    def get_articles(self, pubmed_ids: List[str]) -> List[PubMedArticleSummarized]:
+        summary = None
+        if article.basic_summary:
+            summary = article.basic_summary
+        elif article.abstract:
+            summary = article.abstract[0:500]
+        summary_opt = "summary {summary}" if summary else ""
+        url_opt = f"from {article.article_url}" if article.article_url else ""
+        return f"PubMed DOI {article.doi} title {article.title} {url_opt} {summary_opt}"
+
+    async def get_articles(
+        self, pubmed_ids: List[str]
+    ) -> List[PubMedArticleSummarized]:
         """Get PubMed articles by their IDs."""
         if not pubmed_ids:
             return []
@@ -271,103 +310,95 @@ def get_articles(self, pubmed_ids: List[str]) -> List[PubMedArticleSummarized]:
             try:
                 # Look for existing articles in the database first
                 matching_articles = PubMedArticleSummarized.objects.filter(pmid=pmid)
-                if matching_articles.exists():
-                    article = matching_articles.first()
+                if await matching_articles.aexists():
+                    article = await matching_articles.afirst()
                     if article is not None:
                         pubmed_docs.append(article)
                 else:
                     # Article not in database, fetch it
-                    fetched = pubmed_fetcher.article_by_pmid(pmid)
-                    if fetched is not None:
-                        article = PubMedArticleSummarized.objects.create(
-                            pmid=pmid,
-                            doi=fetched.doi if hasattr(fetched, "doi") else "",
-                            title=(
-                                fetched.title.replace("\x00", "")
-                                if hasattr(fetched, "title") and fetched.title
-                                else ""
-                            ),
-                            abstract=(
-                                fetched.abstract.replace("\x00", "")
-                                if hasattr(fetched, "abstract") and fetched.abstract
-                                else ""
-                            ),
-                            text=(
-                                fetched.content.text.replace("\x00", "")
-                                if hasattr(fetched, "content")
-                                and hasattr(fetched.content, "text")
-                                and fetched.content.text
-                                else ""
-                            ),
-                        )
+                    article = await self.do_article_summary(pmid)
+                    if article is not None:
                         pubmed_docs.append(article)
             except Exception as e:
                 logger.debug(f"Skipping {pmid}: {e}")
 
         return pubmed_docs
 
-    def do_article_summary(self, article_id) -> Optional[PubMedArticleSummarized]:
-        possible_articles = PubMedArticleSummarized.objects.filter(
-            pmid=article_id,
-            basic_summary__isnull=False,
-        )[:1]
-
-        article = None
-        if possible_articles:
-            article = possible_articles[0]
+    async def do_article_summary(self, article_id) -> Optional[PubMedArticleSummarized]:
+        article: Optional[PubMedArticleSummarized] = (
+            await PubMedArticleSummarized.objects.filter(
+                pmid=article_id,
+                basic_summary__isnull=False,
+            ).afirst()
+        )
 
         if article is None:
             try:
                 fetched = pubmed_fetcher.article_by_pmid(article_id)
-                src = FindIt(article_id)
-                url = src.url
                 article_text = ""
-
-                if url is not None:
-                    response = requests.get(url)
-                    if (
-                        ".pdf" in url
-                        or response.headers.get("Content-Type") == "application/pdf"
-                    ):
-                        with tempfile.NamedTemporaryFile(
-                            suffix=".pdf", delete=False
-                        ) as my_data:
-                            my_data.write(response.content)
-
-                            open_pdf_file = open(my_data.name, "rb")
-                            read_pdf = PyPDF2.PdfReader(open_pdf_file)
-                            if read_pdf.is_encrypted:
-                                read_pdf.decrypt("")
-                                for page in read_pdf.pages:
-                                    article_text += page.extract_text()
-                            else:
-                                for page in read_pdf.pages:
-                                    article_text += page.extract_text()
-                    else:
-                        article_text += response.text
-                elif (
-                    fetched
+                try:
+                    src = FindIt(article_id)
+                    url = src.url
+
+                    if url is not None:
+                        async with aiohttp.ClientSession() as session:
+                            async with session.get(url) as response:
+                                response.raise_for_status()
+                                if (
+                                    ".pdf" in url
+                                    or response.headers.get("Content-Type")
+                                    == "application/pdf"
+                                ):
+                                    with tempfile.NamedTemporaryFile(
+                                        suffix=".pdf", delete=False
+                                    ) as my_data:
+                                        my_data.write(await response.read())
+
+                                        open_pdf_file = open(my_data.name, "rb")
+                                        read_pdf = PyPDF2.PdfReader(open_pdf_file)
+                                        if read_pdf.is_encrypted:
+                                            read_pdf.decrypt("")
+                                        for page in read_pdf.pages:
+                                            article_text += page.extract_text()
+                                else:
+                                    # Assume maybe text-ish
+                                    text = (await response.text()).strip()
+                                    if " " in text and len(text) > 50:
+                                        article_text = text
+                except Exception as e:
+                    logger.debug("Error trying to get full text")
+                if (
+                    (article_text is None or article_text == "")
+                    and fetched
                     and hasattr(fetched, "content")
                     and hasattr(fetched.content, "text")
                 ):
                     article_text = fetched.content.text
 
+                title = (fetched.title if hasattr(fetched, "title") else "").strip()
+                abstract = (
+                    fetched.abstract if hasattr(fetched, "abstract") else ""
+                ).strip()
+                article_text = article_text.strip()
+
                 if fetched is not None and (
-                    (hasattr(fetched, "abstract") and fetched.abstract) or article_text
+                    (
+                        hasattr(fetched, "abstract")
+                        and fetched.abstract
+                        and len(fetched.abstract) > 40
+                    )
+                    or (article_text and len(article_text) > 40)
                 ):
-                    article = PubMedArticleSummarized.objects.create(
+                    article = await PubMedArticleSummarized.objects.acreate(
                         pmid=article_id,
                         doi=fetched.doi if hasattr(fetched, "doi") else "",
-                        title=fetched.title if hasattr(fetched, "title") else "",
-                        abstract=(
-                            fetched.abstract if hasattr(fetched, "abstract") else ""
-                        ),
+                        title=title,
+                        abstract=abstract,
                         text=article_text,
                         article_url=url,
-                        basic_summary=ml_router.summarize(
-                            abstract=(
-                                fetched.abstract if hasattr(fetched, "abstract") else ""
-                            ),
+                        basic_summary=await ml_router.summarize(
+                            title=title,
+                            abstract=abstract,
                             text=article_text,
                         ),
                     )
@@ -382,7 +413,7 @@ def article_as_pdf(self, article: PubMedArticleSummarized) -> Optional[str]:
         """Return the best PDF we can find of the article."""
         # First we try and fetch the article
         try:
-            with Timeout(15.0) as _timeout_ctx:
+            with Timeout(20.0) as _timeout_ctx:
                 article_id = article.pmid
                 url = article.article_url
                 if url is not None:
diff --git a/fighthealthinsurance/rest_serializers.py b/fighthealthinsurance/rest_serializers.py
index 71b55c593..16bc3d70f 100644
--- a/fighthealthinsurance/rest_serializers.py
+++ b/fighthealthinsurance/rest_serializers.py
@@ -500,6 +500,11 @@ class GetCandidateArticlesSerializer(serializers.Serializer):
     denial_id = serializers.IntegerField(required=True)
 
 
-class SelectArticlesSerializer(serializers.Serializer):
+class SelectContextArticlesSerializer(serializers.Serializer):
     denial_id = serializers.IntegerField(required=True)
     pmids = serializers.ListField(child=serializers.CharField(), required=True)
+
+
+class SelectAppealArticlesSerializer(serializers.Serializer):
+    appeal_id = serializers.IntegerField(required=True)
+    pmids = serializers.ListField(child=serializers.CharField(), required=True)
diff --git a/fighthealthinsurance/rest_views.py b/fighthealthinsurance/rest_views.py
index dc63e91e0..19a9b7624 100644
--- a/fighthealthinsurance/rest_views.py
+++ b/fighthealthinsurance/rest_views.py
@@ -9,6 +9,8 @@
 from dateutil.relativedelta import relativedelta
 from django.http import FileResponse
 
+from asgiref.sync import sync_to_async, async_to_sync
+
 from django_encrypted_filefield.crypt import Cryptographer
 
 from rest_framework import status
@@ -55,6 +57,8 @@
 from .utils import is_convertible_to_int
 import json
 
+from loguru import logger
+
 if typing.TYPE_CHECKING:
     from django.contrib.auth.models import User
 else:
@@ -109,12 +113,13 @@ def get_serializer_class(self):
         elif self.action == "get_candidate_articles":
             return serializers.GetCandidateArticlesSerializer
         elif self.action == "select_articles":
-            return serializers.SelectArticlesSerializer
+            return serializers.SelectContextArticlesSerializer
         else:
             return None
 
     @extend_schema(responses=serializers.DenialResponseInfoSerializer)
     def create(self, request: Request) -> Response:
+        logger.debug("Routing create through parent...")
         return super().create(request)
 
     @extend_schema(responses=serializers.DenialResponseInfoSerializer)
@@ -133,11 +138,13 @@ def retrieve(self, request: Request, pk: int) -> Response:
 
     @extend_schema(responses=serializers.DenialResponseInfoSerializer)
     def perform_create(self, request: Request, serializer):
+        logger.debug("Performing the create.....")
         current_user: User = request.user  # type: ignore
         creating_professional = ProfessionalUser.objects.get(user=current_user)
         serializer = self.deserialize(data=request.data)
         serializer.is_valid(raise_exception=True)
         serializer_data = serializer.validated_data
+        logger.debug(f"Using data {serializer_data}")
         if (
             "primary_professional" in serializer_data
             and serializer_data["primary_professional"] is not None
@@ -149,15 +156,17 @@ def perform_create(self, request: Request, serializer):
             serializer_data["primary_professional"] = primary_professional
         denial: Optional[Denial] = None
         if "denial_id" in serializer_data:
-            if serializer_data["denial_id"] and is_convertible_to_int(
-                serializer_data["denial_id"]
-            ):
-                denial_id = int(serializer_data.pop("denial_id"))
+            denial_id = serializer_data.pop("denial_id")
+            if denial_id and is_convertible_to_int(denial_id):
+                logger.debug("Looking up existing denial {denial_id}")
+                denial_id = int(denial_id)
                 denial = Denial.filter_to_allowed_denials(current_user).get(
                     denial_id=denial_id
                 )
             else:
-                del serializer_data["denial_id"]
+                logger.debug("Unexpected format of denial id {denial_id}")
+        else:
+            logger.debug("No denial id present, will make new one.")
         if "patient_id" in serializer_data and is_convertible_to_int(
             serializer_data["patient_id"]
         ):
@@ -205,7 +214,7 @@ def get_candidate_articles(self, request: Request) -> Response:
             denial_id=serializer.validated_data["denial_id"],
         )
 
-        articles = pubmed_tools.find_pubmed_articles_for_denial(denial)
+        articles = async_to_sync(pubmed_tools.find_pubmed_articles_for_denial)(denial)
 
         return Response(
             serializers.PubMedMiniArticleSerializer(articles, many=True).data,
@@ -226,7 +235,7 @@ def select_articles(self, request: Request) -> Response:
         )
 
         pmids = serializer.validated_data["pmids"]
-        denial.pubmed_ids_json = json.dumps(pmids)
+        denial.pubmed_ids_json = pmids
         denial.save()
         return Response(
             serializers.SuccessSerializer(
@@ -326,9 +335,34 @@ def get_serializer_class(self):
             return serializers.NotifyPatientRequestSerializer
         elif self.action == "invite_provider":
             return serializers.InviteProviderSerializer
+        elif self.action == "select_articles":
+            return serializers.SelectAppealArticlesSerializer
         else:
             return None
 
+    @extend_schema(responses=serializers.StatusResponseSerializer)
+    @action(detail=False, methods=["post"])
+    def select_articles(self, request: Request) -> Response:
+        """Select PubMed articles to include in the fax."""
+        serializer = self.deserialize(data=request.data)
+        serializer.is_valid(raise_exception=True)
+
+        current_user: User = request.user  # type: ignore
+        appeal = get_object_or_404(
+            Appeal.filter_to_allowed_appeals(current_user),
+            id=serializer.validated_data["appeal_id"],
+        )
+
+        pmids = serializer.validated_data["pmids"]
+        appeal.pubmed_ids_json = pmids
+        appeal.save()
+        return Response(
+            serializers.SuccessSerializer(
+                {"message": f"Selected {len(pmids)} articles for this appeal"}
+            ).data,
+            status=status.HTTP_200_OK,
+        )
+
     @extend_schema(responses=serializers.AppealSummarySerializer)
     def list(self, request: Request) -> Response:
         # Lets figure out what appeals they _should_ see
diff --git a/fighthealthinsurance/websockets.py b/fighthealthinsurance/websockets.py
index e2a34cdd2..1ec428301 100644
--- a/fighthealthinsurance/websockets.py
+++ b/fighthealthinsurance/websockets.py
@@ -22,16 +22,21 @@ async def receive(self, text_data):
         data = json.loads(text_data)
         logger.debug("Starting generation of appeals...")
         aitr = common_view_logic.AppealsBackendHelper.generate_appeals(data)
-        await asyncio.sleep(1)
-        await self.send("\n")
-        async for record in aitr:
-            await asyncio.sleep(0)
-            await self.send("\n")
-            await asyncio.sleep(0)
-            logger.debug(f"Sending record {record}")
-            await self.send(record)
-            await asyncio.sleep(0)
+        # We do a try/except here to log since the WS framework swallow exceptions sometimes
+        try:
+            await asyncio.sleep(1)
             await self.send("\n")
+            async for record in aitr:
+                await asyncio.sleep(0)
+                await self.send("\n")
+                await asyncio.sleep(0)
+                logger.debug(f"Sending record {record}")
+                await self.send(record)
+                await asyncio.sleep(0)
+                await self.send("\n")
+        except Exception as e:
+            logger.opt(exception=True).debug(f"Error sending back appeals: {e}")
+            raise e
         logger.debug("All sent")
         await asyncio.sleep(1)
         await self.close()
diff --git a/scripts/run_local.sh b/scripts/run_local.sh
index b3f19b86f..0c27bd31c 100755
--- a/scripts/run_local.sh
+++ b/scripts/run_local.sh
@@ -72,7 +72,7 @@ python manage.py loaddata plan_source
 python manage.py ensure_adminuser --username admin --password admin
 
 # Make a test user with UserDomain and everything
-python manage.py make_user  --username "test@test.com" --domain testfarts1 --password farts12345678 --email "test@test.com" --visible-phone-number 42 --is-provider true
+python manage.py make_user  --username "test@test.com" --domain testfarts1 --password farts12345678 --email "test@test.com" --visible-phone-number 42 --is-provider true --first-name TestFarts
 python manage.py make_user  --username "test-patient@test.com" --domain testfarts1 --password farts12345678 --email "test-patient@test.com" --visible-phone-number 42 --is-provider false
 
 RECAPTCHA_TESTING=true OAUTHLIB_RELAX_TOKEN_SCOPE=1 uvicorn fighthealthinsurance.asgi:application --reload --reload-dir fighthealthinsurance --reload-include="*.py" --reload-exclude "*.pyc,__pycache__/*,*.pyo,*~,#*#,.#*,node_modules,static" --access-log --log-config conf/uvlog_config.yaml --port 8000 --ssl-keyfile key.pem --ssl-certfile cert.pem $@
diff --git a/tests/async-unit/test_common_view_logic.py b/tests/async-unit/test_common_view_logic.py
index c16a35c88..c58101c27 100644
--- a/tests/async-unit/test_common_view_logic.py
+++ b/tests/async-unit/test_common_view_logic.py
@@ -103,6 +103,7 @@ async def test():
 
         async_to_sync(test)()
 
+    @pytest.mark.skip("Skip for now until we enable this.")
     @pytest.mark.django_db
     @patch("fighthealthinsurance.common_view_logic.appealGenerator")
     async def test_generate_appeal_questions(self, mock_appeal_generator):
diff --git a/tests/sync/test_pubmed_api.py b/tests/sync/test_pubmed_api.py
index b1af25c5c..cec4fe528 100644
--- a/tests/sync/test_pubmed_api.py
+++ b/tests/sync/test_pubmed_api.py
@@ -4,11 +4,13 @@
 import json
 import unittest.mock as mock
 from datetime import datetime, timedelta
+from unittest.mock import AsyncMock
 
 from django.urls import reverse
 from django.contrib.auth import get_user_model
 from django.utils import timezone
-from django.test import TestCase
+from django.test import TestCase, TransactionTestCase
+from asgiref.sync import async_to_sync, sync_to_async
 
 from rest_framework import status
 from rest_framework.test import APITestCase
@@ -98,7 +100,10 @@ def setUp(self):
         self.article1 = PubMedMiniArticle.objects.create(
             pmid="12345678",
             title="Effectiveness of physical therapy for rheumatoid arthritis",
-            abstract="This study demonstrates the effectiveness of physical therapy for patients with rheumatoid arthritis...",
+            abstract="""
+            This study demonstrates the effectiveness of physical therapy for patients with rheumatoid arthritis...
+            We need this to be loooong like over 500 chars so it triggers the summary
+            """ + str(list(range(0, 500))),
             article_url="https://pubmed.ncbi.nlm.nih.gov/12345678/",
         )
 
@@ -171,29 +176,35 @@ def test_get_candidate_articles(self):
         """Test retrieving candidate PubMed articles for a denial."""
         url = reverse("denials-get-candidate-articles")
 
-        response = self.client.post(
-            url,
-            json.dumps({"denial_id": self.denial.denial_id}),
-            content_type="application/json",
-        )
-
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        data = response.json()
+        # Mock the REST view's dependency on pubmed_tools module
+        with mock.patch(
+            "fighthealthinsurance.rest_views.pubmed_tools"
+        ) as mock_pubmed_tools:
+            # Create a coroutine that returns our test articles
+            async def mock_find_pubmed_articles(*args, **kwargs):
+                return [self.article1, self.article2]
+
+            # Set up the mock to return our coroutine function
+            mock_pubmed_tools.find_pubmed_articles_for_denial = (
+                mock_find_pubmed_articles
+            )
 
-        # Should find at least the articles we created
-        self.assertGreaterEqual(len(data), 2)
+            # Make the REST API request
+            response = self.client.post(
+                url,
+                json.dumps({"denial_id": self.denial.denial_id}),
+                content_type="application/json",
+            )
 
-        # Check if our test articles are included
-        pmids = [article["pmid"] for article in data]
-        self.assertIn(self.article1.pmid, pmids)
-        self.assertIn(self.article2.pmid, pmids)
+            # Verify the REST API response
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+            data = response.json()
+            self.assertGreaterEqual(len(data), 2)
 
-        # Verify article structure
-        for article in data:
-            if article["pmid"] == self.article1.pmid:
-                self.assertEqual(article["title"], self.article1.title)
-                self.assertEqual(article["abstract"], self.article1.abstract)
-                self.assertEqual(article["article_url"], self.article1.article_url)
+            # Verify the response contains our articles
+            pmids = [article["pmid"] for article in data]
+            self.assertIn(self.article1.pmid, pmids)
+            self.assertIn(self.article2.pmid, pmids)
 
     def test_select_articles(self):
         """Test selecting PubMed articles for a denial."""
@@ -215,7 +226,7 @@ def test_select_articles(self):
 
         # Verify the denial was updated with selected PMIDs
         updated_denial = Denial.objects.get(denial_id=self.denial.denial_id)
-        saved_pmids = json.loads(updated_denial.pubmed_ids_json)
+        saved_pmids = updated_denial.pubmed_ids_json
         self.assertEqual(saved_pmids, selected_pmids)
 
     def test_assemble_appeal_with_pubmed_articles(self):
@@ -223,10 +234,6 @@ def test_assemble_appeal_with_pubmed_articles(self):
         # First, set up PubMed articles to use
         selected_pmids = ["12345678", "87654321"]
 
-        # Update the denial with the selected PubMed IDs
-        self.denial.pubmed_ids_json = json.dumps(selected_pmids)
-        self.denial.save()
-
         # Create the URL for assemble_appeal endpoint
         url = reverse("appeals-assemble-appeal")
 
@@ -258,7 +265,7 @@ def test_assemble_appeal_with_pubmed_articles(self):
 
             # Verify the appeal was created with the PubMed IDs
             appeal = Appeal.objects.get(id=appeal_id)
-            self.assertEqual(appeal.pubmed_ids_json, json.dumps(selected_pmids))
+            self.assertEqual(appeal.pubmed_ids_json, selected_pmids)
             self.assertEqual(appeal.for_denial, self.denial)
 
             # Verify that the appeal assembly method was called with the right parameters
@@ -278,166 +285,26 @@ def test_coordinate_with_appeal(self):
             creating_professional=self.professional,
         )
 
-        url = reverse("denials-select-articles")
+        url = reverse("appeals-select-articles")
         selected_pmids = ["12345678"]
 
         response = self.client.post(
             url,
-            json.dumps({"denial_id": self.denial.denial_id, "pmids": selected_pmids}),
+            json.dumps({"appeal_id": appeal.id, "pmids": selected_pmids}),
             content_type="application/json",
         )
 
         self.assertEqual(response.status_code, status.HTTP_200_OK)
 
-        # Update the appeal with the selected articles manually
-        appeal.pubmed_ids_json = json.dumps(selected_pmids)
-        appeal.save()
-
         # Verify the appeal was updated
-        updated_appeal = Appeal.objects.get(id=appeal.id)
-        self.assertEqual(updated_appeal.pubmed_ids_json, json.dumps(selected_pmids))
-
-    @mock.patch("fighthealthinsurance.utils.pubmed_fetcher.pmids_for_query")
-    def test_find_pubmed_article_ids_for_query(self, mock_pmids_for_query):
-        """Test the find_pubmed_article_ids_for_query function."""
-        # Mock the PubMed fetcher to avoid real API calls
-        mock_pmids_for_query.return_value = ["99998888", "77776666"]
+        appeal.refresh_from_db()
+        self.assertEqual(appeal.pubmed_ids_json, selected_pmids)
 
-        pubmed_tools = PubMedTools()
-
-        # Test using an existing cached query
-        query = "physical therapy rheumatoid arthritis"
-        since = None
-
-        # Call the function with the correct parameters
-        pmids = pubmed_tools.find_pubmed_article_ids_for_query(query, since)
-
-        # Should get results from cache without calling the API
-        mock_pmids_for_query.assert_not_called()
-        self.assertIn("12345678", pmids)
-        self.assertIn("87654321", pmids)
-
-        # Test with a new query that's not cached
-        new_query = "unique query with no cache"
-
-        # Mock implementation for the since_list to control test behavior
-        with mock.patch.object(pubmed_tools, "since_list", new=["2023"]):
-            pmids = pubmed_tools.find_pubmed_article_ids_for_query(
-                new_query, since="2023"
-            )
-
-            # Should call the API once for the one item in the mocked since_list
-            mock_pmids_for_query.assert_called_once_with(new_query, since="2023")
-
-            # Should get results from the mock function
-            self.assertIn("99998888", pmids)
-            self.assertIn("77776666", pmids)
-
-    def test_find_candidate_articles_for_denial_using_cached_data(self):
-        """Test find_pubmed_articles_for_denial using cached data."""
-        pubmed_tools = PubMedTools()
-
-        # First call creates cache if needed
-        articles = pubmed_tools.find_pubmed_articles_for_denial(self.denial)
-
-        # Verify we got article objects back
-        pmids = [article.pmid for article in articles]
-        self.assertIn(self.article1.pmid, pmids)
-        self.assertIn(self.article2.pmid, pmids)
-
-        # Create another denial with same procedure/diagnosis to test cache sharing
-        denial2 = Denial.objects.create(
-            denial_text="Another denial with same condition",
-            primary_professional=self.professional,
-            patient_user=self.patient,
-            procedure=self.denial.procedure,
-            diagnosis=self.denial.diagnosis,
-        )
-
-        # This should use the cached data from the previous call
-        with mock.patch(
-            "fighthealthinsurance.utils.pubmed_fetcher.pmids_for_query"
-        ) as mock_fetch:
-            articles2 = pubmed_tools.find_pubmed_articles_for_denial(denial2)
-            # Should not need to fetch new data
-            mock_fetch.assert_not_called()
-
-        # Should get same articles
-        pmids2 = [article.pmid for article in articles2]
-        self.assertEqual(set(pmids), set(pmids2))
-
-    def test_find_context_for_denial_with_selected_pmids(self):
-        """Test that find_context_for_denial prioritizes selected PMIDs."""
-        # Set selected PMIDs on the denial
-        selected_pmids = ["11112222", "33334444"]  # Using our additional test articles
-        self.denial.pubmed_ids_json = json.dumps(selected_pmids)
-        self.denial.save()
-
-        # Create mock for the article summary function to avoid actual processing
-        with mock.patch.object(PubMedTools, "do_article_summary") as mock_summary:
-            # Configure the mock to return appropriate objects
-            def mock_summary_side_effect(pmid):
-                # Return an existing summarized article
-                return PubMedArticleSummarized.objects.get(pmid=pmid)
-
-            mock_summary.side_effect = mock_summary_side_effect
-
-            # Also mock out article search to ensure it's not used
-            with mock.patch.object(
-                PubMedTools, "find_pubmed_articles_for_denial"
-            ) as mock_search:
-                pubmed_tools = PubMedTools()
-                context = pubmed_tools.find_context_for_denial(self.denial)
-
-                # Should not have called regular search
-                mock_search.assert_not_called()
-
-                # Context should contain our article summaries
-                for pmid in selected_pmids:
-                    self.assertIn(f"Summary for {pmid}", context)
-
-    def test_old_query_data_refresh(self):
-        """Test that query data older than a month is refreshed."""
-        # Create an old query (more than 30 days old)
-        old_query = "physical therapy rheumatoid arthritis stale"
-        old_query_data = PubMedQueryData.objects.create(
-            query=old_query,
-            articles=json.dumps(["55556666"]),
-            denial_id=self.denial,
-            created=timezone.now() - timedelta(days=35),  # Older than 30 days
-        )
-
-        # Mock the PubMed fetcher
-        with mock.patch(
-            "fighthealthinsurance.utils.pubmed_fetcher.pmids_for_query"
-        ) as mock_fetch:
-            mock_fetch.return_value = ["99998888"]
-
-            # Mock the since_list to ensure predictable behavior
-            pubmed_tools = PubMedTools()
-            with mock.patch.object(pubmed_tools, "since_list", new=["2023"]):
-                pmids = pubmed_tools.find_pubmed_article_ids_for_query(
-                    old_query, since="2023"
-                )
-
-                # Should have called the API because the cached data is too old
-                mock_fetch.assert_called_once_with(old_query, since="2023")
-
-                # Results should include the new mocked data
-                self.assertIn("99998888", pmids)
-
-                # A new query data record should have been created
-                self.assertTrue(
-                    PubMedQueryData.objects.filter(
-                        query=old_query, created__gte=timezone.now() - timedelta(days=1)
-                    ).exists()
-                )
-
-    def test_appeal_assembly_with_pubmed_ids(self):
+    def test_appeal_assembly_with_pubmed_ids_basic(self):
         """Test that AppealAssemblyHelper properly includes PubMed articles when assembling appeals."""
         # Set up test data
         selected_pmids = ["12345678", "87654321"]
-        self.denial.pubmed_ids_json = json.dumps(selected_pmids)
+        self.denial.pubmed_ids_json = selected_pmids
         self.denial.save()
 
         # Create a test mock for Appeal.objects.create to avoid DB operations
@@ -446,7 +313,7 @@ def test_appeal_assembly_with_pubmed_ids(self):
         ) as mock_create_appeal:
             # Set up the mock to return a mock Appeal object
             mock_appeal = mock.MagicMock()
-            mock_appeal.pubmed_ids_json = json.dumps(selected_pmids)
+            mock_appeal.pubmed_ids_json = selected_pmids
             mock_create_appeal.return_value = mock_appeal
 
             # Mock _assemble_appeal_pdf to test the correct passing of pubmed_ids_parsed
@@ -473,9 +340,7 @@ def test_appeal_assembly_with_pubmed_ids(self):
                 # Verify that the appeal was created with the PubMed IDs
                 mock_create_appeal.assert_called_once()
                 create_kwargs = mock_create_appeal.call_args[1]
-                self.assertEqual(
-                    create_kwargs["pubmed_ids_json"], json.dumps(selected_pmids)
-                )
+                self.assertEqual(create_kwargs["pubmed_ids_json"], selected_pmids)
 
                 # Verify that _assemble_appeal_pdf was called with pubmed_ids_parsed
                 mock_assemble_pdf.assert_called_once()
@@ -484,93 +349,174 @@ def test_appeal_assembly_with_pubmed_ids(self):
                 self.assertEqual(call_args["pubmed_ids_parsed"], selected_pmids)
 
 
-class PubMedToolsUnitTest(TestCase):
-    """Unit tests for PubMedTools class."""
+# Class for direct PubMedTools async function testing
+class PubMedToolsAsyncTest(TransactionTestCase):
+    """Direct testing of PubMedTools async methods using the real implementation."""
 
     def setUp(self):
-        self.pubmed_tools = PubMedTools()
-
-        # Create test articles
-        self.article1 = PubMedMiniArticle.objects.create(
-            pmid="12345678",
-            title="Test Article 1",
-            abstract="Test abstract 1",
-        )
-
-        self.article2 = PubMedMiniArticle.objects.create(
-            pmid="87654321",
-            title="Test Article 2",
-            abstract="Test abstract 2",
-        )
-
-        # Create a test denial
-        self.denial = Denial.objects.create(
-            denial_text="Test denial",
-            procedure="test procedure",
-            diagnosis="test diagnosis",
+        # Create a denial with procedure and diagnosis
+        self.test_denial = Denial.objects.create(
+            denial_text="Test denial for arthritis requiring physical therapy",
+            procedure="physical therapy",
+            diagnosis="rheumatoid arthritis",
         )
 
-        # Create cached query data
-        self.query_data = PubMedQueryData.objects.create(
-            query="test procedure test diagnosis",
-            articles=json.dumps(["12345678"]),
-            denial_id=self.denial,
+        # Create cached query data to avoid real API calls
+        PubMedQueryData.objects.create(
+            query="physical therapy rheumatoid arthritis",
+            articles=json.dumps(["12345678", "87654321"]),
+            denial_id=self.test_denial,
+            created=timezone.now(),
         )
 
-        # Create corresponding PubMedArticleSummarized objects
-        PubMedArticleSummarized.objects.create(
+        # Create test articles that would be returned from the API
+        PubMedMiniArticle.objects.create(
             pmid="12345678",
-            title="Test Article 1",
-            abstract="Test abstract 1",
-            basic_summary="Summary for 12345678",
+            title="Effectiveness of physical therapy for rheumatoid arthritis",
+            abstract="This study demonstrates the effectiveness of physical therapy...",
+            article_url="https://pubmed.ncbi.nlm.nih.gov/12345678/",
         )
 
         PubMedArticleSummarized.objects.create(
-            pmid="87654321",
-            title="Test Article 2",
-            abstract="Test abstract 2",
-            basic_summary="Summary for 87654321",
+            pmid="12345678",
+            title="Effectiveness of physical therapy for rheumatoid arthritis",
+            abstract="""This study demonstrates the effectiveness of physical therapy...""",
+            doi="10.1000/12345678",
+            basic_summary="Summary about physical therapy for rheumatoid arthritis",
         )
 
     @mock.patch("fighthealthinsurance.utils.pubmed_fetcher.pmids_for_query")
-    def test_find_pubmed_article_ids_empty_results(self, mock_pmids_for_query):
-        """Test handling of empty results from PubMed API."""
-        # First call returns empty list, second call returns some results
-        mock_pmids_for_query.side_effect = [[], ["99998888"]]
-
-        # Mock the since_list to ensure predictable behavior
-        with mock.patch.object(self.pubmed_tools, "since_list", new=["2022", "2023"]):
-            # Test with a new query
-            new_query = "query with no initial results"
-            pmids = self.pubmed_tools.find_pubmed_article_ids_for_query(
-                new_query, since=None
+    @mock.patch("fighthealthinsurance.ml.ml_router.ml_router.summarize")
+    def test_find_pubmed_article_ids_for_query_real(
+        self, mock_summarize, mock_pmids_for_query
+    ):
+        """Test the real async method with minimal mocking."""
+        # Mock external API call
+        mock_pmids_for_query.return_value = ["99998888", "77776666"]
+
+        # Mock ML router summarize to avoid ML dependency
+        async def mock_summarize_impl(*args, **kwargs):
+            return "Mock summarized text"
+
+        mock_summarize.side_effect = mock_summarize_impl
+
+        # Run the actual async method
+        async def run_test():
+            pubmed_tools = PubMedTools()
+            # First test: query that's already cached
+            cached_pmids = await pubmed_tools.find_pubmed_article_ids_for_query(
+                "physical therapy rheumatoid arthritis"
+            )
+            # Should return cached results without calling the API
+            self.assertIn("12345678", cached_pmids)
+            self.assertIn("87654321", cached_pmids)
+            mock_pmids_for_query.assert_not_called()
+
+            # Second test: new query that's not cached
+            mock_pmids_for_query.reset_mock()
+            new_pmids = await pubmed_tools.find_pubmed_article_ids_for_query(
+                "unique query with no cache"
             )
+            # Should call the API and return its results
+            mock_pmids_for_query.assert_called_once()
+            self.assertEqual(new_pmids, ["99998888", "77776666"])
 
-            # Should still get results from second call
-            self.assertIn("99998888", pmids)
+        # Run the async test in a sync context
+        async_to_sync(run_test)()
 
-            # Should have called the API for each "since" value in our mocked since_list
-            self.assertEqual(mock_pmids_for_query.call_count, 2)
+    @mock.patch("fighthealthinsurance.ml.ml_router.ml_router.summarize")
+    def test_find_context_for_denial_real(self, mock_summarize):
+        """Test the real find_context_for_denial method with minimal mocking."""
 
-    def test_get_articles(self):
-        """Test retrieving PubMedArticleSummarized objects by PMID."""
-        # Test with existing articles
-        pmids = ["12345678", "87654321"]
-        articles = self.pubmed_tools.get_articles(pmids)
+        # Configure mocks for external dependencies
+        async def mock_summarize_impl(*args, **kwargs):
+            return "Mock summarized text with article information"
 
-        self.assertEqual(len(articles), 2)
-        article_pmids = [article.pmid for article in articles]
-        self.assertIn("12345678", article_pmids)
-        self.assertIn("87654321", article_pmids)
+        mock_summarize.side_effect = mock_summarize_impl
 
-        # Test with empty list
-        articles = self.pubmed_tools.get_articles([])
-        self.assertEqual(len(articles), 0)
+        # Set up selected PMIDs on denial
+        self.test_denial.pubmed_ids_json = ["12345678"]
+        self.test_denial.save()
 
-        # Test with non-existent article (would require mocking the fetcher)
-        with mock.patch(
-            "fighthealthinsurance.utils.pubmed_fetcher.article_by_pmid"
-        ) as mock_fetch:
-            mock_fetch.return_value = None
-            articles = self.pubmed_tools.get_articles(["99999999"])
-            self.assertEqual(len(articles), 0)
+        # Run the actual async method
+        async def run_test():
+            pubmed_tools = PubMedTools()
+            context = await pubmed_tools.find_context_for_denial(self.test_denial)
+
+            # Verify summarize was called with context including our article
+            mock_summarize.assert_called_once()
+            # Return value from the mock
+            self.assertEqual(context, "Mock summarized text with article information")
+
+        # Run the async test in a sync context
+        async_to_sync(run_test)()
+
+    @mock.patch("fighthealthinsurance.utils.pubmed_fetcher.article_by_pmid")
+    @mock.patch("fighthealthinsurance.ml.ml_router.ml_router.summarize")
+    def test_get_articles_real(self, mock_summarize, mock_article_by_pmid):
+        """Test the real get_articles method."""
+
+        # Configure mocks
+        async def mock_summarize_impl(*args, **kwargs):
+            return "Mock summarized text"
+
+        mock_summarize.side_effect = mock_summarize_impl
+
+        class MockPubMedArticle:
+            def __init__(self):
+                self.pmid = "99999999"
+                self.title = "New Test Article"
+                self.abstract = "New test abstract"
+                self.doi = "10.1000/99999999"
+                self.content = mock.MagicMock()
+                self.content.text = "Full article text"
+
+        mock_article_by_pmid.return_value = MockPubMedArticle()
+
+        # Run the actual async method
+        async def run_test():
+            pubmed_tools = PubMedTools()
+            # Test getting existing articles from database
+            articles = await pubmed_tools.get_articles(["12345678"])
+
+            # Should find the article in the database
+            self.assertEqual(len(articles), 1)
+            self.assertEqual(articles[0].pmid, "12345678")
+            mock_article_by_pmid.assert_not_called()
+
+            # Test fetching a new article not in database
+            mock_article_by_pmid.reset_mock()
+            with mock.patch("metapub.FindIt") as mock_findit:
+                mock_findit_instance = mock.MagicMock()
+                mock_findit_instance.url = "https://example.com/article.pdf"
+                mock_findit.return_value = mock_findit_instance
+
+                with mock.patch("requests.get") as mock_get:
+                    mock_response = mock.MagicMock()
+                    mock_response.ok = True
+                    mock_response.headers = {"Content-Type": "text/html"}
+                    mock_response.text = "<html>Article text</html>"
+                    mock_get.return_value = mock_response
+
+                    # Call with a PMID not in database
+                    with mock.patch.object(PubMedTools, "do_article_summary"):
+                        # Skip actual article summary creation
+                        pubmed_tools.do_article_summary = AsyncMock(
+                            return_value=PubMedArticleSummarized(
+                                pmid="99999999",
+                                title="New Test Article",
+                                abstract="New test abstract",
+                                doi="10.1000/99999999",
+                                basic_summary="Mock summary",
+                            )
+                        )
+
+                        new_articles = await pubmed_tools.get_articles(["99999999"])
+
+                        # Should have called method to fetch it
+                        self.assertEqual(len(new_articles), 1)
+                        self.assertEqual(new_articles[0].pmid, "99999999")
+                        self.assertEqual(new_articles[0].title, "New Test Article")
+
+        # Run the async test in a sync context
+        async_to_sync(run_test)()
diff --git a/tests/sync/test_rest_api.py b/tests/sync/test_rest_api.py
index 27ad7bcb3..f7685579b 100644
--- a/tests/sync/test_rest_api.py
+++ b/tests/sync/test_rest_api.py
@@ -1187,3 +1187,85 @@ def test_get_full_details_unauthorized_user(self):
 
         # Should return 404 because this user doesn't have access to this appeal
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+
+from typing import Any, Dict
+
+
+class DenialCreateWithExistingId(APITestCase):
+    """Test creating a denial with an existing denial id."""
+
+    fixtures = ["./fighthealthinsurance/fixtures/initial.yaml"]
+
+    def setUp(self):
+        self.domain = UserDomain.objects.create(
+            name="testdomain",
+            visible_phone_number="1234567890",
+            internal_phone_number="0987654321",
+            active=True,
+            display_name="Test Domain",
+            business_name="Test Business",
+            country="USA",
+            state="CA",
+            city="Test City",
+            address1="123 Test St",
+            zipcode="12345",
+        )
+        self.user = User.objects.create_user(
+            username=f"testuser{self.domain.id}",
+            password="testpass",
+            email="test@example.com",
+        )
+        self.username = f"testuser{self.domain.id}"
+        self.password = "testpass"
+        self.prouser = ProfessionalUser.objects.create(
+            user=self.user, active=True, npi_number="1234567890"
+        )
+        self.user.is_active = True
+        self.user.save()
+        ExtraUserProperties.objects.create(user=self.user, email_verified=True)
+
+    def test_create_with_existing_denial_id(self):
+        login_result = self.client.login(username=self.username, password=self.password)
+        self.assertTrue(login_result)
+        # Create a denial
+        url = reverse("denials-list")
+        email = "timbit@fighthealthinsurance.com"
+        hashed_email = hashlib.sha512(email.encode("utf-8")).hexdigest()
+        denial_text = "Test denial text"
+        response = self.client.post(
+            url,
+            json.dumps(
+                {
+                    "email": email,
+                    "denial_text": denial_text,
+                    "pii": "true",
+                    "tos": "true",
+                    "privacy": "true",
+                    "store_raw_email": "true",
+                }
+            ),
+            content_type="application/json",
+        )
+        self.assertTrue(status.is_success(response.status_code))
+        parsed: Dict[str, Any] = response.json()
+        denial_id = parsed["denial_id"]
+        # Create another denial with the same denial id
+        response = self.client.post(
+            url,
+            json.dumps(
+                {
+                    "email": email,
+                    "denial_text": denial_text,
+                    "pii": "true",
+                    "tos": "true",
+                    "privacy": "true",
+                    "store_raw_email": "true",
+                    "denial_id": denial_id,
+                }
+            ),
+            content_type="application/json",
+        )
+        self.assertTrue(status.is_success(response.status_code))
+        parsed = response.json()
+        self.assertEqual(parsed["denial_id"], denial_id)


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (9)</summary><blockquote>

<details>
<summary>fighthealthinsurance/management/commands/make_user.py (1)</summary><blockquote>

`61-61`: **Validate necessity of the default first name.**  
Using `options.get("first_name", "test_first_name")` contradicts “required=True,” as the argument must be provided. Eliminate the default if you want to enforce a strict requirement.  

```diff
- first_name = options.get("first_name", "test_first_name")
+ first_name = options["first_name"]
```

</blockquote></details>
<details>
<summary>scripts/run_local.sh (1)</summary><blockquote>

`75-75`: **Optionally use a more neutral first name in dev scripts.**  
Having “TestFarts” is amusing, but you might choose a more neutral placeholder if you anticipate external visibility of this script.

</blockquote></details>
<details>
<summary>fighthealthinsurance/websockets.py (1)</summary><blockquote>

`25-39`: **Consider refining sleep calls for performance.**  
Your new try/except block effectively logs exceptions. However, multiple `await asyncio.sleep(0)` calls in quick succession can degrade throughput. If these sleeps are only meant to yield control to the event loop, you might combine them or remove them to reduce overhead.

</blockquote></details>
<details>
<summary>fighthealthinsurance/ml/ml_router.py (1)</summary><blockquote>

`75-79`: **Consolidate repeated inference logic and improve type clarity.**  
1. Merging `abstract` + `text` and then retrying with `abstract` alone is fine, though a helper function could reduce repetition.  
2. Star imports obscure where `RemoteModelLike` originates. Explicitly import or define it to avoid confusion.  

```diff
-from fighthealthinsurance.ml.ml_models import *
+from fighthealthinsurance.ml.ml_models import (
+  RemoteModelLike,
+  ModelDescription,
+  candidate_model_backends,
+  # ... other needed imports
+)
```


Also applies to: 81-81, 85-85, 87-87, 89-101

</blockquote></details>
<details>
<summary>fighthealthinsurance/models.py (1)</summary><blockquote>

`442-442`: **Evaluate the necessity of `null=True` for the boolean field.**

Since `flag_for_exclude` is already given a default of `False`, consider removing `null=True` unless you truly need a third ‘unknown’ state.


```diff
- flag_for_exclude = models.BooleanField(default=False, null=True)
+ flag_for_exclude = models.BooleanField(default=False)
```

</blockquote></details>
<details>
<summary>fighthealthinsurance/common_view_logic.py (1)</summary><blockquote>

`404-406`: **Add error handling for the async function call.**

Wrapping `async_to_sync(pmt.get_articles)(pubmed_ids_parsed)` in a try-except block can help gracefully handle potential exceptions (e.g., network timeouts, database errors) to ensure `_assemble_appeal_pdf` continues to function smoothly.


```diff
+ try:
    pubmed_docs: list[PubMedArticleSummarized] = async_to_sync(
        pmt.get_articles
    )(pubmed_ids_parsed)
+ except Exception as e:
+     logger.error(f"Failed to retrieve PubMed articles: {e}")
+     pubmed_docs = []
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

404-404: `PubMedArticleSummarized` may be undefined, or defined from star imports

(F405)

</details>

</details>

</blockquote></details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (3)</summary><blockquote>

`6-6`: **Remove unused import `async_to_sync`.**  
The function `async_to_sync` is not referenced in the codebase. Removing it helps maintain a cleaner import list.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

</details>

</details>

---

`9-9`: **Remove unused import `Future`.**  
`Future` is imported from `concurrent.futures` but not used, so consider removing it to avoid confusion.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

</details>

</details>

---

`114-116`: **Consider elevating log level.**  
`logger.opt(exception=True).debug()` might hide severe issues. A higher level (e.g., `.error()`) could better surface critical errors.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 80541b732662fe3cf92541206e0f0b151a5332ca and 138a76e2f853bfa6533708936db1b12b3bc80ed2.

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `fighthealthinsurance/common_view_logic.py` (5 hunks)
* `fighthealthinsurance/management/commands/make_user.py` (3 hunks)
* `fighthealthinsurance/migrations/0090_denial_flag_for_exclude_and_more.py` (1 hunks)
* `fighthealthinsurance/ml/ml_router.py` (1 hunks)
* `fighthealthinsurance/models.py` (1 hunks)
* `fighthealthinsurance/pubmed_tools.py` (14 hunks)
* `fighthealthinsurance/websockets.py` (1 hunks)
* `scripts/run_local.sh` (1 hunks)
* `tests/sync/test_pubmed_api.py` (3 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>tests/sync/test_pubmed_api.py (1)</summary><blockquote>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (5) (5)</summary>

* `find_pubmed_articles_for_denial` (120-189)
* `find_pubmed_article_ids_for_query` (31-118)
* `find_context_for_denial` (191-277)
* `get_articles` (291-318)
* `do_article_summary` (320-392)

</details>

</blockquote></details>
<details>
<summary>fighthealthinsurance/common_view_logic.py (3)</summary><blockquote>

<details>
<summary>fighthealthinsurance/models.py (2) (2)</summary>

* `PubMedArticleSummarized` (273-286)
* `Denial` (372-498)

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (3) (3)</summary>

* `get_articles` (291-318)
* `find_pubmed_articles_for_denial` (120-189)
* `find_context_for_denial` (191-277)

</details>
<details>
<summary>fhi_users/models.py (2) (2)</summary>

* `get_legal_name` (163-164)
* `get_full_name` (205-206)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>fighthealthinsurance/ml/ml_router.py</summary>

74-74: `RemoteModelLike` may be undefined, or defined from star imports

(F405)

</details>
<details>
<summary>tests/sync/test_pubmed_api.py</summary>

193-193: SyntaxError: Unexpected indentation

---

356-356: SyntaxError: Expected a statement

</details>
<details>
<summary>fighthealthinsurance/common_view_logic.py</summary>

404-404: `PubMedArticleSummarized` may be undefined, or defined from star imports

(F405)

---

1299-1299: `Denial` may be undefined, or defined from star imports

(F405)

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

---

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker django containers
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: types
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker django containers
* GitHub Check: Build the docker ray containers

</details>

<details>
<summary>🔇 Additional comments (41)</summary><blockquote>

<details>
<summary>fighthealthinsurance/management/commands/make_user.py (2)</summary>

`30-32`: **Ensure fallback logic aligns with the required argument.**  
Declaring `--first-name` as a required argument typically removes the need for fallback logic in the command. It's still acceptable to keep a fallback for special debugging scenarios, but confirm that you actually need it here.  

Could you verify whether there’s a use case that bypasses command-line parsing, making the fallback worthwhile? If not, consider removing the fallback or making the command truly optional.

---

`106-106`: **Good integration of first_name into user creation.**  
This line correctly sets `first_name` for the `User` object, fulfilling the new parameter’s intended usage.

</details>
<details>
<summary>fighthealthinsurance/ml/ml_router.py (1)</summary>

`71-73`: <details>
<summary>❓ Verification inconclusive</summary>

**Asynchronous method introduction looks solid.**  
Making `summarize` async fits the broader async updates. Confirm all call sites are also updated to await it properly.

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for existing summarize calls outside ml_router to ensure it's always awaited.
rg -A 2 "summarize("
```

Length of output: 93

---

Below is a revised review comment that incorporates updated verification steps. It asks you to re-run the search using properly escaped regex so you can confirm that all `summarize` call sites are updated to be awaited:

---

**Verify that all `summarize` call sites are properly awaiting the asynchronous method.**  
The asynchronous method in `fighthealthinsurance/ml/ml_router.py` (lines 71–73) looks good. However, the previous regex failed because the literal parenthesis wasn’t escaped. Please re-run the following commands to check that every call to `summarize` is preceded by an `await`:

- **To locate any non-awaited calls (expecting zero results):**
  
  ```shell
  
  #!/bin/bash
  # Using a Perl-compatible negative lookbehind (-P) to find calls to `summarize(` not preceded by "await".
  echo "Searching for non-awaited summarize calls (this should ideally return nothing):"
  rg -P -A 2 '(?<!await\s+)summarize\(' || true
  ```

- **To list all awaited calls for cross-reference:**

  ```shell
  
  #!/bin/bash
  echo "Searching for awaited summarize calls:"
  rg -A 2 'await\s+summarize\(' || true
  ```

Please manually verify that all `summarize` invocations are appropriately awaited. 



---

**Action: Verify that every call to `summarize` is awaited.**  
The new asynchronous implementation for `summarize` in `fighthealthinsurance/ml/ml_router.py` (lines 71–73) looks correct. However, our previous search encountered a regex error due to unescaped parentheses. Please run the following updated shell scripts to confirm that all call sites to `summarize` are properly awaited:

- **Check for non-awaited calls:**  
  This script looks for instances of `summarize(` that are not preceded by `await` (using a negative lookbehind).

  ```shell
  
  #!/bin/bash
  echo "Searching for non-awaited summarize calls (should return no results):"
  rg -P -A 2 '(?<!await\s)summarize\(' || true
  ```

- **List awaited calls (for cross-reference):**  

  ```shell
  
  #!/bin/bash
  echo "Listing awaited summarize calls:"
  rg -A 2 'await\s+summarize\(' || true
  ```

Please run these scripts and manually verify that every invocation of `summarize` is properly awaited.  
  


---

**Action: Verify that every call to the asynchronous `summarize` method is properly awaited.**  
The asynchronous implementation in `fighthealthinsurance/ml/ml_router.py` (lines 71–73) appears correct. However, to ensure consistency across the codebase, please confirm that every invocation of `summarize` is preceded by an `await` statement.

To facilitate this, please run the following shell scripts:

- **Check for non-awaited calls:**  
  This script searches for instances of `summarize(` that are not preceded by `await` (using a negative lookbehind). Ideally, it should produce no output.
  
  ```shell
  
  #!/bin/bash
  echo "Searching for non-awaited summarize calls (should return no results):"
  rg -P -A 2 '(?<!await\s)summarize\(' || true
  ```

- **List awaited calls:**  
  This script lists contexts where `await summarize(` is used to cross-reference and confirm correct usage.
  
  ```shell
  
  #!/bin/bash
  echo "Listing awaited summarize calls:"
  rg -A 2 'await\s+summarize\(' || true
  ```

Please run these commands and manually verify that every call to `summarize` is properly awaited.



---



</details>

**Action: Confirm that every call to `summarize` is properly awaited**  
The asynchronous implementation in `fighthealthinsurance/ml/ml_router.py` (lines 71–73) looks correct. However, to ensure consistency, please verify that no call to `summarize` appears without the proper `await` keyword. For this, you can run the following shell script commands manually:

- **To locate any non-awaited calls (ideally, this should produce no output):**  
  ```shell
  rg -P -A 2 '(?<!await\s)summarize\(' || true
  ```

- **To list all awaited calls for cross-reference:**  
  ```shell
  rg -A 2 'await\s+summarize\(' || true
  ```

Please manually verify these outputs. If any non-awaited usages are found, update them accordingly.

</details>
<details>
<summary>fighthealthinsurance/migrations/0090_denial_flag_for_exclude_and_more.py (1)</summary>

`37-46`: **All other fields look good.**

The addition of Boolean and TextFields with sensible defaults is consistent with typical Django migration workflows. No further concerns.

</details>
<details>
<summary>tests/sync/test_pubmed_api.py (9)</summary>

`7-13`: **Imports look good.**

Using `AsyncMock` and importing both `TestCase` and `TransactionTestCase` for sync/async tests is appropriate. No issues found.

---

`355-357`: **Class definition for async tests is clear.**

Defining `PubMedToolsAsyncTest(TransactionTestCase)` is a sound approach for integrating database-dependent async tests.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

356-356: SyntaxError: Expected a statement

</details>

</details>

---

`360-364`: **Test data creation is straightforward.**

Creating a `Denial` with relevant fields sets up a realistic scenario. No issues found.

---

`369-372`: **PubMedQueryData setup is fine.**

Preloading query data is a clean way to test caching logic. No issues here.

---

`375-380`: **Pre-creation of `PubMedMiniArticle` is valid.**

Stubbing out necessary mini articles helps ensure tests remain deterministic. Good practice.

---

`392-426`: **Comprehensive test for query caching.**

Mocking `pmids_for_query` and verifying caches in `test_find_pubmed_article_ids_for_query_real` is well-structured. Coverage for both cached and uncached queries looks solid.

---

`428-429`: **Async-to-sync test execution is consistent.**

Calling `async_to_sync(run_test)()` ensures the asynchronous method is tested in a synchronous environment with minimal overhead.

---

`430-456`: **Good coverage of `find_context_for_denial`.**

Mocking `ml_router.summarize` isolates network/ML complexities while validations confirm final context. No issues encountered.

---

`457-526`: **Robust testing of `get_articles`.**

The combination of existing-article retrieval and new-article fetching ensures thorough coverage. The patches for external calls and mocking summary logic appear correct.

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (28)</summary>

`12-12`: **Import usage confirmed.**  
`TimeoutException` is now used in exception handling. This appears correct.

---

`31-36`: **Async method signature update looks good.**  
Increasing the timeout from 30 to 60 seconds might help with slower PubMed queries, but ensure it doesn’t adversely impact user experience.

---

`59-59`: **Async existence check.**  
Using `await existing_queries.aexists()` is a valid approach for non-blocking DB checks.

---

`61-61`: **Iterating with `async for`.**  
This is a correct way to iterate through query results asynchronously.

---

`75-77`: **Convert PubMed fetching to async.**  
Calling `pubmed_fetcher.pmids_for_query` through `sync_to_async` is consistent with your asynchronous design.

---

`81-85`: **Async creation of query data.**  
Storing fetched PubMed IDs with `acreate` is correct and keeps DB operations non-blocking.

---

`105-109`: **Repeated async creation of query data.**  
This matches the prior logic, ensuring results are cached. Consider DRY if creating multiple similar entries in different code paths.

---

`120-121`: **New async method for denial article search.**  
Transition to async plus a 60s timeout is consistent with the approach used elsewhere.

---

`140-140`: **Async call to article IDs method.**  
This call is properly awaited, maintaining non-blocking behavior.

---

`154-156`: **Filtering asynchronously for mini articles.**  
Using `.afirst()` is an efficient way to fetch the first matching item.

---

`167-167`: **Creating new mini article asynchronously.**  
`acreate` ensures you remain fully async in your data creation flow.

---

`183-184`: **Error-level logging on article fetch failure.**  
Elevating to `.error()` is appropriate since these issues may warrant closer inspection.

---

`227-227`: **Awaiting article search for denial.**  
Refactoring to async completes the pipeline consistently.

---

`233-234`: **Saving denial state asynchronously.**  
Using `asave()` is a solid approach for non-blocking writes.

---

`236-241`: **Async list comprehension for summarized articles.**  
This pattern cleanly retrieves matching articles without blocking.

---

`251-251`: **Resetting `missing_pmids`.**  
Setting `missing_pmids = []` ensures a fresh list if articles are already found.

---

`262-262`: **Timeout debug logging.**  
Helps to monitor partial results when the external API is slow.

---

`269-274`: **Asynchronous summarization.**  
Fallback to returning the raw concatenated context if summarization fails is a good fallback strategy.

---

`282-289`: **Conditional summary logic.**  
Truncating long abstracts at 500 characters is an effective approach to avoid overly large strings.

---

`291-293`: **Async method to fetch articles.**  
Switching to `async def` aligns with your broader asynchronous design.

---

`306-307`: **Asynchronous existence check and retrieval.**  
This ensures minimal overhead and fast retrieval of cached articles.

---

`312-313`: **Summary fallback creation.**  
Creating articles on the fly if not found is a seamless approach.

---

`320-326`: **Asynchronous article summary.**  
Properly awaits the existing record check before potentially creating a new summary.

---

`351-351`: **Conditional check for PDF decryption.**  
No concerns here. It appears to handle a specialized scenario.

---

`356-358`: **Hardcoded server error check.**  
This condition helps skip malformed responses. Additional checks might be valuable for other error states, but this is fine.

---

`381-381`: **Async summarization assignment.**  
Assigning `basic_summary` with `await ml_router.summarize(...)` fits the pattern used in other methods.

---

`382-382`: **Passing abstract for summarization.**  
This is consistent with the method’s additional `text` context.

---

`384-385`: **Completing the summarization pipeline.**  
Supplying both `abstract` and `text` ensures thorough summarization coverage.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T19:29:04Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>fighthealthinsurance/models.py (1)</summary><blockquote>

`437-442`: **Consider removing redundant parameters from model fields.**

The `primary_key=False` parameter is redundant, as this is Django's default behavior for model fields. Additionally, there's a pattern of `null=True` with `default=""` which can lead to inconsistent data representation (some records might have NULL, others might have empty strings).

Recommended approach:

```diff
- manual_deidentified_denial = models.TextField(primary_key=False, null=True, default="")
+ manual_deidentified_denial = models.TextField(default="", blank=True)

- manual_deidentified_appeal = models.TextField(primary_key=False, null=True, default="")
+ manual_deidentified_appeal = models.TextField(default="", blank=True)

- manual_searchterm = models.TextField(primary_key=False, null=True, default="")
+ manual_searchterm = models.TextField(default="", blank=True)

- verified_procedure = models.TextField(primary_key=False, null=True, default="")
+ verified_procedure = models.TextField(default="", blank=True)

- verified_diagnosis = models.TextField(primary_key=False, null=True, default="")
+ verified_diagnosis = models.TextField(default="", blank=True)

- flag_for_exclude = models.BooleanField(default=False, null=True)
+ flag_for_exclude = models.BooleanField(default=False)
```

This aligns with the Django best practice of using empty values instead of NULL for string fields, and simplifies the code.

</blockquote></details>
<details>
<summary>tests/sync/test_pubmed_api.py (2)</summary><blockquote>

`12-13`: **Remove unused imports.**

The static analysis identified a few unused imports that should be removed for cleaner code.

```diff
- from django.test import TestCase, TransactionTestCase
+ from django.test import TransactionTestCase

- from asgiref.sync import async_to_sync, sync_to_async
+ from asgiref.sync import async_to_sync
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

12-12: `django.test.TestCase` imported but unused

Remove unused import: `django.test.TestCase`

(F401)

---

13-13: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>

</details>

---

`454-522`: **Consider refactoring complex test into smaller components.**

This test method is quite complex with multiple nested mock contexts. Consider refactoring into smaller, more focused test methods for better maintainability.


You could split it into two separate test methods:
1. One for testing retrieval of existing articles from the database
2. Another for testing fetching and processing new articles

This would make the tests more readable and easier to maintain.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 138a76e2f853bfa6533708936db1b12b3bc80ed2 and 901d7a0e40ed38ad1544833849992f384f8f8bdd.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `fighthealthinsurance/migrations/0090_denial_flag_for_exclude_and_more.py` (1 hunks)
* `fighthealthinsurance/models.py` (1 hunks)
* `tests/sync/test_pubmed_api.py` (3 hunks)
* `tests/sync/test_rest_api.py` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* tests/sync/test_rest_api.py

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* fighthealthinsurance/migrations/0090_denial_flag_for_exclude_and_more.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>tests/sync/test_pubmed_api.py (2)</summary><blockquote>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (4) (4)</summary>

* `find_pubmed_articles_for_denial` (120-189)
* `PubMedTools` (27-461)
* `find_pubmed_article_ids_for_query` (31-118)
* `find_context_for_denial` (191-277)

</details>
<details>
<summary>fighthealthinsurance/models.py (2) (2)</summary>

* `Denial` (372-498)
* `PubMedMiniArticle` (289-299)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>tests/sync/test_pubmed_api.py</summary>

12-12: `django.test.TestCase` imported but unused

Remove unused import: `django.test.TestCase`

(F401)

---

13-13: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

---

190-190: Undefined name `response`

(F821)

---

191-191: Undefined name `response`

(F821)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: types
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Build the docker django containers
* GitHub Check: Build the docker ray containers
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>fighthealthinsurance/models.py (1)</summary>

`437-438`: **Correct the typos in the field names.**

Similar to the migration file, the field names `manual_deidentified_denial` and `manual_deidentified_appeal` were previously identified as having spelling mistakes. These have now been fixed, which is good.

</details>
<details>
<summary>tests/sync/test_pubmed_api.py (3)</summary>

`353-355`: **Good use of TransactionTestCase for async testing.**

Using `TransactionTestCase` is appropriate here since you're testing asynchronous code that may interact with the database outside of a transaction.

---

`389-426`: **Good implementation of async test with proper mocking.**

The test properly mocks external dependencies while testing the actual async behavior of the `find_pubmed_article_ids_for_query` method. Using `async_to_sync` to run the async test in a synchronous context is the correct approach.

---

`427-453`: **Effective testing of find_context_for_denial async method.**

This test effectively verifies that the async method properly processes the denial context and interacts with the ML summarization service.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T21:33:23Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🔭 Outside diff range comments (3)</summary><blockquote>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (3)</summary><blockquote>

`97-111`: _⚠️ Potential issue_

**Potential async conversion missed in the for-loop.**

The code inside the loop that searches with different years still uses the synchronous `pubmed_fetcher.pmids_for_query` directly, while it should use the async version like earlier in the method.

```diff
-                            year_pmids = pubmed_fetcher.pmids_for_query(
-                                query, since=year
-                            )
+                            year_pmids = await sync_to_async(pubmed_fetcher.pmids_for_query)(
+                                query, since=year
+                            )
```

---

`162-167`: _⚠️ Potential issue_

**Potential async conversion missed when fetching articles.**

Similar to the earlier issue, `pubmed_fetcher.article_by_pmid` and `FindIt` are still being called synchronously, which could block the event loop.

```diff
-                            fetched = pubmed_fetcher.article_by_pmid(pmid)
-                            if fetched:
-                                src = FindIt(pmid)
-                                url = src.url
+                            fetched = await sync_to_async(pubmed_fetcher.article_by_pmid)(pmid)
+                            if fetched:
+                                src = await sync_to_async(FindIt)(pmid)
+                                url = src.url
```

---

`330-332`: _⚠️ Potential issue_

**Missing async conversion when fetching article details.**

Similar to earlier issues, `pubmed_fetcher.article_by_pmid` and `FindIt` are still called synchronously.

```diff
-                fetched = pubmed_fetcher.article_by_pmid(article_id)
-                src = FindIt(article_id)
+                fetched = await sync_to_async(pubmed_fetcher.article_by_pmid)(article_id)
+                src = await sync_to_async(FindIt)(article_id)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>fighthealthinsurance/models.py (1)</summary><blockquote>

`437-438`: _⚠️ Potential issue_

**Correct the typos in the field names**

Similar to the migration file, the fields `manual_deidentified_denail` and `manual_deintified_appeal` contain spelling mistakes. Renaming them now will avoid confusion in model usage.



Apply this diff to fix the typos:

```diff
- manual_deidentified_denial = models.TextField(primary_key=False, null=True, default="")
+ manual_deidentified_denial = models.TextField(primary_key=False, null=True, default="")

- manual_deidentified_appeal = models.TextField(primary_key=False, null=True, default="")
+ manual_deidentified_appeal = models.TextField(primary_key=False, null=True, default="")
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>fighthealthinsurance/rest_views.py (1)</summary><blockquote>

`12-12`: **Remove the unused import `sync_to_async`**

The import `sync_to_async` is not being used in this file. Only `async_to_sync` is being used at line 210.

```diff
- from asgiref.sync import sync_to_async, async_to_sync
+ from asgiref.sync import async_to_sync
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

12-12: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>

</details>

</blockquote></details>
<details>
<summary>tests/sync/test_pubmed_api.py (1)</summary><blockquote>

`12-13`: **Unused import detected.**

`TestCase` and `sync_to_async` are imported but not used in this file.

```diff
-from django.test import TestCase, TransactionTestCase
-from asgiref.sync import async_to_sync, sync_to_async
+from django.test import TransactionTestCase
+from asgiref.sync import async_to_sync
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

12-12: `django.test.TestCase` imported but unused

Remove unused import: `django.test.TestCase`

(F401)

---

13-13: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>

</details>

</blockquote></details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (3)</summary><blockquote>

`6-6`: **Unused import detected.**

The `async_to_sync` import is not used in this file.

```diff
-from asgiref.sync import sync_to_async, async_to_sync
+from asgiref.sync import sync_to_async
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

</details>

</details>

---

`9-9`: **Unused import detected.**

The `Future` import from concurrent.futures is not used in this file.

```diff
-from concurrent.futures import Future
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

</details>

</details>

---

`335-359`: **Potential async issue with HTTP requests.**

The `requests.get` call is synchronous and could block the event loop. Consider using an async HTTP client like aiohttp.

```diff
-                    response = requests.get(url)
+                    # Use aiohttp for non-blocking HTTP requests
+                    async with aiohttp.ClientSession() as session:
+                        async with session.get(url) as response:
+                            content = await response.read()
+                            headers = response.headers
+                            status = response.status
+                            text = await response.text()
```

This would require adding:
```python
import aiohttp
```
at the top of the file.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

346-346: Use a context manager for opening files

(SIM115)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 901d7a0e40ed38ad1544833849992f384f8f8bdd and dccd179598fa6c7e0cd1f70d67db720754ece791.

</details>

<details>
<summary>📒 Files selected for processing (11)</summary>

* `fighthealthinsurance/common_view_logic.py` (5 hunks)
* `fighthealthinsurance/management/commands/make_user.py` (3 hunks)
* `fighthealthinsurance/migrations/0090_denial_flag_for_exclude_and_more.py` (1 hunks)
* `fighthealthinsurance/ml/ml_router.py` (1 hunks)
* `fighthealthinsurance/models.py` (1 hunks)
* `fighthealthinsurance/pubmed_tools.py` (14 hunks)
* `fighthealthinsurance/rest_views.py` (2 hunks)
* `fighthealthinsurance/websockets.py` (1 hunks)
* `scripts/run_local.sh` (1 hunks)
* `tests/sync/test_pubmed_api.py` (3 hunks)
* `tests/sync/test_rest_api.py` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (5)</summary>

* tests/sync/test_rest_api.py
* fighthealthinsurance/websockets.py
* scripts/run_local.sh
* fighthealthinsurance/management/commands/make_user.py
* fighthealthinsurance/migrations/0090_denial_flag_for_exclude_and_more.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (3)</summary>

<details>
<summary>fighthealthinsurance/rest_views.py (1)</summary><blockquote>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (1) (1)</summary>

* `find_pubmed_articles_for_denial` (120-189)

</details>

</blockquote></details>
<details>
<summary>tests/sync/test_pubmed_api.py (2)</summary><blockquote>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (4) (4)</summary>

* `find_pubmed_articles_for_denial` (120-189)
* `find_pubmed_article_ids_for_query` (31-118)
* `find_context_for_denial` (191-277)
* `get_articles` (291-318)

</details>
<details>
<summary>fighthealthinsurance/models.py (3) (3)</summary>

* `Denial` (372-498)
* `PubMedMiniArticle` (289-299)
* `PubMedArticleSummarized` (273-286)

</details>

</blockquote></details>
<details>
<summary>fighthealthinsurance/common_view_logic.py (3)</summary><blockquote>

<details>
<summary>fighthealthinsurance/models.py (2) (2)</summary>

* `PubMedArticleSummarized` (273-286)
* `Denial` (372-498)

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (3) (3)</summary>

* `get_articles` (291-318)
* `find_pubmed_articles_for_denial` (120-189)
* `find_context_for_denial` (191-277)

</details>
<details>
<summary>fhi_users/models.py (2) (2)</summary>

* `get_legal_name` (163-164)
* `get_full_name` (205-206)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>fighthealthinsurance/rest_views.py</summary>

12-12: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>
<details>
<summary>fighthealthinsurance/ml/ml_router.py</summary>

74-74: `RemoteModelLike` may be undefined, or defined from star imports

(F405)

</details>
<details>
<summary>tests/sync/test_pubmed_api.py</summary>

12-12: `django.test.TestCase` imported but unused

Remove unused import: `django.test.TestCase`

(F401)

---

13-13: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>
<details>
<summary>fighthealthinsurance/common_view_logic.py</summary>

404-404: `PubMedArticleSummarized` may be undefined, or defined from star imports

(F405)

---

1299-1299: `Denial` may be undefined, or defined from star imports

(F405)

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

---

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker django containers
* GitHub Check: types
* GitHub Check: types
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers

</details>

<details>
<summary>🔇 Additional comments (38)</summary><blockquote>

<details>
<summary>fighthealthinsurance/rest_views.py (1)</summary>

`210-210`: **Good conversion to handle async function call**

This change properly wraps the now-asynchronous `find_pubmed_articles_for_denial` function with `async_to_sync`, allowing it to be called from synchronous code.

</details>
<details>
<summary>fighthealthinsurance/ml/ml_router.py (4)</summary>

`71-73`: **Good asynchronous method conversion with improved parameter handling**

Converting `summarize` to an async method allows for proper concurrent execution. Adding a default value for `abstract` makes the method more flexible.

---

`76-81`: **Improved model selection logic**

The new logic concatenates models from specific sources, potentially providing better fallback options when preferred models are available.

---

`85-87`: **Added prudent input truncation**

Limiting `abstract` and `text` to 1000 characters prevents excessive token usage while preserving the essential content for summarization.

---

`89-101`: **Enhanced resilience with proper async/await pattern and fallback strategy**

The code now correctly awaits the async `_infer` method calls and implements a fallback strategy when the first attempt returns `None`, improving reliability.

</details>
<details>
<summary>fighthealthinsurance/models.py (1)</summary>

`439-442`: **New data fields enhance manual control over denial data**

These additional fields provide useful capabilities for manual search terms, verified procedure/diagnosis information, and exclusion flagging. Ensure these fields are covered by your tests.

</details>
<details>
<summary>fighthealthinsurance/common_view_logic.py (5)</summary>

`404-406`: **Good implementation of async call conversion**

Properly wrapping the async `get_articles` method with `async_to_sync` allows it to be called from synchronous code, aligning with the project's transition to async operations.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

404-404: `PubMedArticleSummarized` may be undefined, or defined from star imports

(F405)

</details>

</details>

---

`1299-1310`: **Excellent performance improvement with select_related**

Using `select_related` for related objects (`patient_user`, `domain`, `primary_professional`) reduces database queries by prefetching related objects in a single query.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

1299-1299: `Denial` may be undefined, or defined from star imports

(F405)

</details>

</details>

---

`1397-1397`: **Good use of direct await on async method**

Properly awaiting the async `find_context_for_denial` method, replacing the previous implementation that may have used `sync_to_async`.

---

`1399-1401`: **Improved error logging with exception context**

Using `logger.opt(exception=True)` captures the full exception traceback, making debugging much easier by providing complete error context.

---

`1454-1461`: **Enhanced template substitution with entity relationships**

The code now properly handles patient, professional, and domain information for template substitution, making better use of the database relationships.

</details>
<details>
<summary>tests/sync/test_pubmed_api.py (6)</summary>

`7-7`: **Good addition of AsyncMock for async test patterns.**

The AsyncMock import is essential for properly mocking asynchronous functions in your test suite. This enables testing the async methods in the PubMedTools class.

---

`176-204`: **Well implemented async pattern for testing!**

Good job refactoring this test to correctly mock the asynchronous behavior. The previous issue with the undefined `response` variable has been properly addressed by moving the API call inside the mock context and ensuring all assertions happen after the response is defined.

---

`359-362`: **Good transition to TransactionTestCase for async testing.**

Using TransactionTestCase is appropriate for testing asynchronous database operations, as it provides proper transaction management for async test methods.

---

`395-433`: **Well structured async test with proper mocking.**

This test effectively validates the caching behavior of the `find_pubmed_article_ids_for_query` method by:
1. Testing cached query results without API calls
2. Testing new queries that trigger API calls
3. Using proper async patterns with `async_to_sync` to run async code in sync context

The mock setup and assertions are clear and comprehensive.

---

`434-459`: **Effective testing of the context generation process.**

This test properly validates the `find_context_for_denial` method's behavior with selected PMIDs, ensuring that:
1. The method calls the summarization function correctly
2. The context returned from the function matches expectations
3. Database operations work as expected in async context

The mock configuration and test structure follow best practices.

---

`461-529`: **Comprehensive test for article retrieval with thorough mocking.**

This test effectively covers multiple scenarios:
1. Retrieving existing articles from the database
2. Fetching new articles not in the database
3. Proper error handling and edge cases

The complex mocking setup is well structured and follows best practices for testing async code.

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (21)</summary>

`31-36`: **Good increase in timeout value.**

Increasing the timeout from 30.0 to 60.0 seconds is appropriate for async operations that might take longer to complete, especially when querying external APIs or processing large datasets.

---

`59-61`: **Proper conversion to async query patterns.**

The code correctly uses `await existing_queries.aexists()` and `async for` to handle database queries asynchronously, which prevents blocking the event loop during database operations.

---

`75-77`: **Good use of sync_to_async for external library function.**

The call to the synchronous `pubmed_fetcher.pmids_for_query` is properly wrapped with `sync_to_async` to integrate it with the async flow without blocking the event loop.

---

`81-85`: **Correctly updated to async database creation.**

Using `await PubMedQueryData.objects.acreate()` instead of the synchronous version ensures that database operations don't block the event loop.

---

`114-116`: **Improved error logging with exception context.**

Using `logger.opt(exception=True).debug()` is a better practice as it includes the full exception traceback in the logs, making debugging easier.

---

`120-122`: **Good increase in timeout value for denial article search.**

Increasing the timeout from 30.0 to 60.0 seconds is appropriate for this operation which might involve multiple external API calls.

---

`140-142`: **Proper awaiting of async method call.**

The code correctly awaits the async `find_pubmed_article_ids_for_query` method, ensuring the proper flow of async operations.

---

`154-156`: **Correctly updated to async database query.**

Using `await PubMedMiniArticle.objects.filter(pmid=pmid).afirst()` ensures database operations are non-blocking.

---

`182-184`: **Improved error logging with exception context.**

Using `logger.opt(exception=True).error()` is a better practice as it includes the full exception traceback in the logs, making debugging easier.

---

`191-192`: **Good increase in timeout value for context generation.**

Increasing the timeout from 30.0 to 60.0 seconds is appropriate for this operation which involves complex processing of multiple articles.

---

`227-229`: **Properly awaiting the async find_pubmed_articles_for_denial method.**

The code correctly awaits the async method call, ensuring the proper flow of async operations.

---

`233-234`: **Good use of async save operation.**

Using `await denial.asave()` instead of the synchronous version ensures database operations don't block the event loop.

---

`236-241`: **Well-structured async list comprehension.**

The use of `async for` in a list comprehension is an elegant way to fetch articles asynchronously from the database.

---

`251-261`: **Fixed logical error in missing PMIDs handling.**

The added condition `elif articles and len(articles) == len(selected_pmids):` with explicit empty list assignment for `missing_pmids` improves the logic clarity.

---

`260-260`: **Properly awaiting the async get_articles method.**

The code correctly awaits the async method call when extending the articles list.

---

`268-275`: **Cleaner article formatting and summarization.**

The refactored code for formatting articles and awaiting the summarization is more readable and has better error handling.

---

`291-293`: **Good async conversion of get_articles method.**

Converting this method to async allows for non-blocking operations when fetching multiple articles.

---

`306-307`: **Properly updated to async database operations.**

Using `await matching_articles.aexists()` and `await matching_articles.afirst()` ensures database queries don't block the event loop.

---

`312-313`: **Properly awaiting the async do_article_summary method.**

The code correctly awaits the async method call and checks its result.

---

`320-326`: **Well-structured async database query in do_article_summary.**

The code correctly uses async database operations to fetch an existing article summary.

---

`371-386`: **Correctly updated to async database creation and ML summarization.**

Using `await PubMedArticleSummarized.objects.acreate()` and awaiting the ML summarization ensures non-blocking operations.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T21:47:20Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>fighthealthinsurance/rest_views.py (1)</summary><blockquote>

`12-12`: **Unused import detected**

The import `sync_to_async` is not used in this file and should be removed.

```diff
-from asgiref.sync import sync_to_async, async_to_sync
+from asgiref.sync import async_to_sync
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

12-12: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>

</details>

</blockquote></details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (2)</summary><blockquote>

`6-6`: **Unused import detected**

The import `async_to_sync` is not used in this file and should be removed.

```diff
-from asgiref.sync import sync_to_async, async_to_sync
+from asgiref.sync import sync_to_async
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

</details>

</details>

---

`9-9`: **Remove unused import**

The `Future` import from `concurrent.futures` is not used in this file.

```diff
-from concurrent.futures import Future
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between dccd179598fa6c7e0cd1f70d67db720754ece791 and 123a73f48a17330cf535046e76de1b76b4ac724b.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `fighthealthinsurance/common_view_logic.py` (7 hunks)
* `fighthealthinsurance/management/commands/make_user.py` (3 hunks)
* `fighthealthinsurance/ml/ml_router.py` (1 hunks)
* `fighthealthinsurance/models.py` (3 hunks)
* `fighthealthinsurance/pubmed_tools.py` (15 hunks)
* `fighthealthinsurance/rest_views.py` (3 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* fighthealthinsurance/management/commands/make_user.py
* fighthealthinsurance/models.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (2)</summary><blockquote>

<details>
<summary>fighthealthinsurance/models.py (4) (4)</summary>

* `PubMedQueryData` (302-309)
* `Denial` (372-503)
* `PubMedMiniArticle` (289-299)
* `PubMedArticleSummarized` (273-286)

</details>
<details>
<summary>fighthealthinsurance/ml/ml_router.py (1) (1)</summary>

* `summarize` (71-106)

</details>

</blockquote></details>
<details>
<summary>fighthealthinsurance/rest_views.py (1)</summary><blockquote>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (1) (1)</summary>

* `find_pubmed_articles_for_denial` (120-189)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>fighthealthinsurance/ml/ml_router.py</summary>

74-74: `RemoteModelLike` may be undefined, or defined from star imports

(F405)

</details>
<details>
<summary>fighthealthinsurance/common_view_logic.py</summary>

399-399: `PubMedArticleSummarized` may be undefined, or defined from star imports

(F405)

---

1400-1400: `Denial` may be undefined, or defined from star imports

(F405)

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

---

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

</details>
<details>
<summary>fighthealthinsurance/rest_views.py</summary>

12-12: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Build the docker django containers
* GitHub Check: Build the docker ray containers
* GitHub Check: types

</details>

<details>
<summary>🔇 Additional comments (20)</summary><blockquote>

<details>
<summary>fighthealthinsurance/rest_views.py (2)</summary>

`210-210`: **LGTM: Proper async handling for PubMed article retrieval**

The conversion to using `async_to_sync` is appropriate here as it allows calling the now-asynchronous `find_pubmed_articles_for_denial` function from this synchronous context.

---

`231-232`: **Improved data structure handling**

This change correctly simplifies the handling of PubMed IDs by directly storing the `pmids` list in the model's JSON field rather than serializing it first.

</details>
<details>
<summary>fighthealthinsurance/ml/ml_router.py (5)</summary>

`71-73`: **LGTM: Conversion to async with default parameter**

Good conversion to an asynchronous method with the addition of a default value for the `abstract` parameter, making the function more flexible.

---

`74-81`: **Better model selection strategy**

The revised logic prioritizes the Llama 3.3 70B Instruct model when available by concatenating it with internal models, improving performance for summarization tasks.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

74-74: `RemoteModelLike` may be undefined, or defined from star imports

(F405)

</details>

</details>

---

`85-87`: **Preventing excessive prompt lengths**

Good improvement to truncate both abstract and text to 1000 characters, which helps avoid token limitations when using LLMs.

---

`89-105`: **Robust fallback mechanism for article summarization**

The implementation now properly awaits model inference and includes a fallback approach - first trying with both text and abstract, then with just the abstract if the first attempt fails. This increases the likelihood of successful summarization.

---

`74-74`: **Explicitly import RemoteModelLike**

The type hint `RemoteModelLike` is imported from a star import, making it harder to track dependencies.

For clarity and better static analysis, please explicitly import RemoteModelLike:

```python
from fighthealthinsurance.ml.ml_models import RemoteModelLike
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

74-74: `RemoteModelLike` may be undefined, or defined from star imports

(F405)

</details>

</details>

</details>
<details>
<summary>fighthealthinsurance/common_view_logic.py (5)</summary>

`289-289`: **Simplified data handling for pubmed_ids_json**

Direct assignment of `pubmed_ids_parsed` to `pubmed_ids_json` field aligns with the updated model field definition that now uses JSONField.

---

`309-310`: **Consistent handling of pubmed_ids in appeal updates**

The direct assignment of `pubmed_ids_parsed` to `appeal.pubmed_ids_json` maintains consistency with the create operation.

---

`399-401`: **Proper async handling for PubMed article retrieval**

Good use of `async_to_sync` to properly call the now-asynchronous `get_articles` method while maintaining the synchronous interface of this method.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

399-399: `PubMedArticleSummarized` may be undefined, or defined from star imports

(F405)

</details>

</details>

---

`1399-1411`: **Improved database query with select_related**

Excellent optimization using `select_related` to prefetch related objects. This will significantly reduce the number of database queries when accessing related fields like `patient_user`, `domain`, and `primary_professional`.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

1400-1400: `Denial` may be undefined, or defined from star imports

(F405)

</details>

</details>

---

`1498-1502`: **Better error handling with improved logging**

The change properly awaits the now-asynchronous `find_context_for_denial` method and improves error logging by using `logger.opt(exception=True)` to capture the full exception traceback.

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (8)</summary>

`31-36`: **Increased timeout for better reliability**

Good change to increase the default timeout from 30 to 60 seconds and convert the method to async. PubMed API calls can be slow, especially with complex queries, so this gives operations more time to complete.

---

`120-122`: **Enhanced async implementation for article discovery**

Converting this method to async with increased timeout improves the overall throughput of the application by allowing other operations to proceed while waiting for PubMed responses.

---

`191-191`: **Proper async conversion for context retrieval**

Excellent change to convert this method to async, which will allow for non-blocking operation when gathering context for denials.

---

`205-208`: **Simplified handling of pubmed_ids_json**

This change correctly accesses `pubmed_ids_json` as a direct list instead of parsing it from JSON, aligning with the model field update in the models.py file.

---

`268-276`: **Enhanced article summarization with ML-based fallbacks**

This implementation improves the context generation by:
1. Joining article contexts into a single string
2. Attempting to summarize with ML router
3. Falling back to the raw joined contexts if summarization fails

This provides a more robust approach to generating useful context.

---

`291-293`: **Async conversion for article retrieval**

Converting this method to async improves performance by allowing parallel processing of article retrievals.

---

`320-326`: **Optimized article summary retrieval**

The async conversion and improved filtering (adding `basic_summary__isnull=False`) ensures we only retrieve articles that already have summaries, reducing unnecessary processing.

---

`380-386`: **Proper async integration with ml_router**

The code now correctly awaits the asynchronous `ml_router.summarize` method when generating article summaries, passing both abstract and full text for optimal results.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T00:26:33Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>fighthealthinsurance/rest_views.py (1)</summary><blockquote>

`12-12`: **Remove unused import**

The `sync_to_async` import is not being used in this file.

```diff
-from asgiref.sync import sync_to_async, async_to_sync
+from asgiref.sync import async_to_sync
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

12-12: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>

</details>

</blockquote></details>
<details>
<summary>tests/sync/test_pubmed_api.py (1)</summary><blockquote>

`12-13`: **Remove unused imports**

There are two unused imports that should be removed:
- `TestCase` from `django.test` is imported but not used
- `sync_to_async` from `asgiref.sync` is imported but not used

```diff
-from django.test import TestCase, TransactionTestCase
+from django.test import TransactionTestCase
-from asgiref.sync import async_to_sync, sync_to_async
+from asgiref.sync import async_to_sync
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

12-12: `django.test.TestCase` imported but unused

Remove unused import: `django.test.TestCase`

(F401)

---

13-13: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 123a73f48a17330cf535046e76de1b76b4ac724b and ea8d04480a205814ddd2d128a616fcaf936eafcc.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `fighthealthinsurance/migrations/0091_denial_flag_for_exclude_and_more.py` (1 hunks)
* `fighthealthinsurance/migrations/0092_alter_appeal_pubmed_ids_json_alter_faxestosend_pmids.py` (1 hunks)
* `fighthealthinsurance/rest_serializers.py` (1 hunks)
* `fighthealthinsurance/rest_views.py` (5 hunks)
* `tests/sync/test_pubmed_api.py` (8 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* fighthealthinsurance/migrations/0092_alter_appeal_pubmed_ids_json_alter_faxestosend_pmids.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>fighthealthinsurance/migrations/0091_denial_flag_for_exclude_and_more.py (1)</summary><blockquote>

<details>
<summary>fighthealthinsurance/migrations/0092_alter_appeal_pubmed_ids_json_alter_faxestosend_pmids.py (1) (1)</summary>

* `Migration` (6-23)

</details>

</blockquote></details>
<details>
<summary>fighthealthinsurance/rest_views.py (4)</summary><blockquote>

<details>
<summary>fighthealthinsurance/rest_serializers.py (1) (1)</summary>

* `SelectAppealArticlesSerializer` (508-510)

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (1) (1)</summary>

* `find_pubmed_articles_for_denial` (120-189)

</details>
<details>
<summary>fighthealthinsurance/rest_mixins.py (1) (1)</summary>

* `deserialize` (16-18)

</details>
<details>
<summary>fighthealthinsurance/models.py (1) (1)</summary>

* `Appeal` (529-630)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>fighthealthinsurance/rest_views.py</summary>

12-12: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>
<details>
<summary>tests/sync/test_pubmed_api.py</summary>

12-12: `django.test.TestCase` imported but unused

Remove unused import: `django.test.TestCase`

(F401)

---

13-13: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker django containers
* GitHub Check: Build the docker ray containers
* GitHub Check: types

</details>

<details>
<summary>🔇 Additional comments (14)</summary><blockquote>

<details>
<summary>fighthealthinsurance/rest_serializers.py (2)</summary>

`503-505`: **Good refactoring for clarity: Renamed serializer to better reflect its purpose**

The rename from `SelectArticlesSerializer` to `SelectContextArticlesSerializer` makes the purpose of this serializer clearer - it's specifically for selecting context articles related to a denial.

---

`508-510`: **Great addition: New serializer for appeal-specific article selection**

This new serializer provides a dedicated class for handling appeal-related article selections, following a consistent pattern with the context serializer but with the appropriate `appeal_id` field instead of `denial_id`.

</details>
<details>
<summary>fighthealthinsurance/migrations/0091_denial_flag_for_exclude_and_more.py (1)</summary>

`13-47`: **Model enhancements with flexible fields added**

The migration adds several useful fields to the `denial` model that will improve data flexibility:
- Flag for excluding denials from processing
- Fields for manual de-identification of content
- Fields for verified diagnosis and procedure 
- The migration to JSONField for `pubmed_ids_json` aligns with modern Django practices and provides better data integrity than string serialization

This change is well-structured with appropriate default values and nullability.

</details>
<details>
<summary>fighthealthinsurance/rest_views.py (4)</summary>

`114-114`: **Updated serializer reference**

The reference has been correctly updated to match the renamed serializer class in `rest_serializers.py`.

---

`210-210`: **Good asynchronous implementation for PubMed article retrieval**

Converting the call to `find_pubmed_articles_for_denial` to be asynchronous is a good improvement. Using `async_to_sync` is the right approach to call an async function from synchronous code.

---

`231-231`: **Direct JSON field assignment**

The change to directly assign the list to `pubmed_ids_json` instead of using JSON serialization is appropriate since the field has been migrated to a JSONField.

---

`336-357`: **Good addition: Appeal-specific article selection**

This new method in the `AppealViewSet` follows the same pattern as the one in `DenialViewSet`, creating a consistent developer experience. It properly leverages the new `SelectAppealArticlesSerializer` and follows the same response structure as the denial version.

</details>
<details>
<summary>tests/sync/test_pubmed_api.py (7)</summary>

`176-204`: **Well-implemented async test mocking**

The test has been properly updated to mock the asynchronous nature of the `find_pubmed_articles_for_denial` function. The approach of defining a mock async function and assigning it to the mocked dependency is correct. Excellent handling of this non-trivial testing scenario.

---

`226-227`: **Updated assertion to work with JSON field**

The test has been correctly updated to work with the new JSONField instead of parsing JSON strings.

---

`297-298`: **Updated handling of JSONField in test**

Good adjustment to directly access the JSON data from the field without parsing.

---

`349-350`: **Good class update for async testing**

Changing to `TransactionTestCase` is appropriate for testing async functionality as it ensures database transactions are handled correctly in the async context.

---

`387-422`: **Well-structured async test for article ID retrieval**

This test effectively validates both cached and non-cached scenarios for the async `find_pubmed_article_ids_for_query` method. The use of nested async functions with `async_to_sync` is the right approach for testing async code in a sync test framework.

---

`425-449`: **Good test for async denial context retrieval**

This test properly validates the async `find_context_for_denial` method, mocking external dependencies while testing the actual async workflow. Well-structured with proper setup and assertions.

---

`453-519`: **Comprehensive test for article retrieval**

This test effectively covers multiple scenarios for the async `get_articles` method:
1. Retrieving articles already in the database
2. Fetching new articles not in the database

The extensive mocking of external dependencies is well-implemented to isolate the test from network calls and focus on the functionality.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T00:49:18Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>fighthealthinsurance/common_view_logic.py (2)</summary><blockquote>

`413-425`: **Excellent query optimization with select_related.**

Adding `select_related` to prefetch related objects (`patient_user`, `domain`, and `primary_professional`) reduces the number of database queries and improves performance. This is particularly valuable when these related fields are accessed later in the function.

Consider also using `prefetch_related` for many-to-many relationships if they're accessed in the code flow.

---

`399-399`: **Consider explicit import for PubMedArticleSummarized.**

`PubMedArticleSummarized` is currently imported through a star import, which could be made explicit for better code clarity.

```diff
-from fighthealthinsurance.models import *
+from fighthealthinsurance.models import Denial, Appeal, PubMedArticleSummarized, ... # Add other needed imports
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

399-399: `PubMedArticleSummarized` may be undefined, or defined from star imports

(F405)

</details>

</details>

</blockquote></details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (2)</summary><blockquote>

`6-6`: **Remove unused import.**

The `async_to_sync` import is not used in this file. This was identified by the static analysis tool and should be removed.

```diff
-from asgiref.sync import sync_to_async, async_to_sync
+from asgiref.sync import sync_to_async
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

</details>

</details>

---

`9-9`: **Remove unused import.**

The `Future` import from `concurrent.futures` is not used in this file. This was identified by the static analysis tool and should be removed.

```diff
-from concurrent.futures import Future
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ea8d04480a205814ddd2d128a616fcaf936eafcc and 6c92fd79abb53e6ad7d1121460508e6dc4bd43d3.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `fighthealthinsurance/common_view_logic.py` (11 hunks)
* `fighthealthinsurance/generate_appeal.py` (3 hunks)
* `fighthealthinsurance/management/commands/make_user.py` (3 hunks)
* `fighthealthinsurance/ml/ml_models.py` (6 hunks)
* `fighthealthinsurance/ml/ml_router.py` (1 hunks)
* `fighthealthinsurance/pubmed_tools.py` (13 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* fighthealthinsurance/management/commands/make_user.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (2)</summary><blockquote>

<details>
<summary>fighthealthinsurance/models.py (4) (4)</summary>

* `PubMedQueryData` (302-309)
* `Denial` (372-503)
* `PubMedMiniArticle` (289-299)
* `PubMedArticleSummarized` (273-286)

</details>
<details>
<summary>fighthealthinsurance/ml/ml_router.py (1) (1)</summary>

* `summarize` (71-106)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>fighthealthinsurance/ml/ml_router.py</summary>

74-74: `RemoteModelLike` may be undefined, or defined from star imports

(F405)

---

87-87: Local variable `text_optional` is assigned to but never used

Remove assignment to unused variable `text_optional`

(F841)

</details>
<details>
<summary>fighthealthinsurance/common_view_logic.py</summary>

399-399: `PubMedArticleSummarized` may be undefined, or defined from star imports

(F405)

---

1414-1414: `Denial` may be undefined, or defined from star imports

(F405)

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

---

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: types
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers

</details>

<details>
<summary>🔇 Additional comments (52)</summary><blockquote>

<details>
<summary>fighthealthinsurance/ml/ml_router.py (4)</summary>

`71-73`: **Good update to make the method asynchronous and provide a default value.**

Making `summarize` asynchronous allows for better integration with the application's async workflow. The default value for `abstract` improves function usability.

---

`75-81`: **Good addition of specialized model selection.**

The changes to model selection logic to prioritize the "google/gemma-3-27b-it" model when available is a good enhancement that could improve summarization quality.

---

`89-105`: **Good use of asynchronous method calls and proper error handling.**

The code now properly awaits the `_infer` method calls rather than using `asyncio.run`, which is the correct approach in an async method. The fallback mechanism attempting to summarize with only the abstract if the first attempt fails is also a good design choice for robustness.

---

`74-74`: **Consider explicitly importing RemoteModelLike.**

As noted by static analysis and previous reviews, `RemoteModelLike` is imported via a star import rather than explicitly.

```diff
-from fighthealthinsurance.ml.ml_models import *
+from fighthealthinsurance.ml.ml_models import RemoteModelLike, ModelDescription
```

This would improve code clarity and aid static analysis tools.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

74-74: `RemoteModelLike` may be undefined, or defined from star imports

(F405)

</details>

</details>

</details>
<details>
<summary>fighthealthinsurance/generate_appeal.py (3)</summary>

`48-56`: **Good enhancement of the method signature with procedure and diagnosis parameters.**

Adding `procedure` and `diagnosis` as parameters to `get_appeal_questions` provides valuable context that can help generate more relevant and specific questions for the health insurance appeal.

---

`76-82`: **Correctly passing the new parameters to the model.**

The new parameters are properly passed to the model's `get_appeal_questions` method, ensuring the procedure and diagnosis information is utilized in question generation.

---

`95-100`: **Consistent parameter passing in the fallback case.**

The new parameters are also correctly passed in the fallback case when the patient context is not available, ensuring consistent behavior.

</details>
<details>
<summary>fighthealthinsurance/common_view_logic.py (6)</summary>

`289-289`: **Good simplification of JSON field handling.**

Direct assignment to `pubmed_ids_json` eliminates unnecessary JSON serialization, suggesting a proper model field type change from CharField to JSONField.

---

`308-309`: **Consistent handling of pubmed_ids_json field in update flow.**

The update case now also directly assigns `pubmed_ids_parsed` to the JSON field, maintaining consistency with the creation flow.

---

`399-401`: **Good use of async_to_sync for asynchronous PubMed article retrieval.**

Using `async_to_sync` to call the now-asynchronous `get_articles` method allows for maintaining the synchronous signature of `_assemble_appeal_pdf` while leveraging asynchronous capabilities.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

399-399: `PubMedArticleSummarized` may be undefined, or defined from star imports

(F405)

</details>

</details>

---

`1512-1512`: **Direct await call improves code clarity.**

Changing to directly `await` the `find_context_for_denial` method instead of using `sync_to_async` simplifies the code and indicates proper async conversion of the method.

---

`1514-1516`: **Improved error logging with exception context.**

Using `logger.opt(exception=True).debug` instead of simple debug logs captures the exception traceback, making debugging easier.

---

`1125-1131`: **Improved timeout handling for optional extraction tasks.**

The timeout for optional extraction tasks is now explicitly set to 45 seconds, with better error handling and logging. This ensures the process doesn't hang indefinitely on extraction tasks that aren't critical.

</details>
<details>
<summary>fighthealthinsurance/ml/ml_models.py (6)</summary>

`71-76`: **Good enhancement of method signature with procedure and diagnosis parameters.**

Adding `procedure` and `diagnosis` parameters to the interface method provides valuable context that can help generate more relevant and specific questions for health insurance appeals. This matches the changes seen in `generate_appeal.py`.

---

`719-721`: **Well-documented method parameters.**

The docstring has been properly updated to describe the new parameters, which enhances code maintainability and clarity.

---

`249-251`: **Improved bad response detection.**

Adding more phrases to the `generic_bad_ideas` list helps identify and filter out potentially problematic or hallucinated responses from the model. This improves the quality control of generated content.

---

`726-744`: **Well-structured prompt generation with conditional content.**

The prompt generation now intelligently includes procedure and diagnosis information when available, and structures the prompt in a clear way. The use of optional sections based on available data is a good design pattern.

---

`664-686`: **Excellent improvement to system prompt guidance.**

The updated system prompt for question generation provides much more detailed guidelines including:
- Clear explanation of the goal and context
- Well-defined do's and don'ts
- Good and bad examples
- Format preferences

This level of detail should significantly improve the quality and relevance of generated questions.

---

`943-947`: **Good addition of new model to supported models list.**

Adding support for the "google/gemma-3-27b-it" model in the `DeepInfra` class complements the model selection logic in `ml_router.py`. This coordination across files demonstrates good attention to detail in the implementation.

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (33)</summary>

`27-28`: **LGTM: Extracted constant to improve visibility.**

Good practice to define the `PER_QUERY` constant at the top level for clarity and maintainability.

---

`32-32`: **LGTM: Updated since_list to include 2024.**

Keeping the since_list updated with the current year is a good practice to ensure searches include recent publications.

---

`34-39`: **LGTM: Increased timeout and converted to async method.**

The method has been properly converted to async and the timeout has been increased from 30.0 to 60.0 seconds. This should help reduce timeout issues when fetching PubMed articles.

---

`62-64`: **LGTM: Properly converted to async database operations.**

The code has been updated to use async database operations (`aexists()` and async iterator) which is necessary for async compatibility.

---

`78-80`: **LGTM: Proper use of sync_to_async for synchronous function.**

Using `sync_to_async` to wrap the synchronous `pubmed_fetcher.pmids_for_query` function is the correct approach for calling sync code from an async context.

---

`84-88`: **LGTM: Updated to use async database creation.**

The code now correctly uses `acreate()` instead of synchronous `create()`.

---

`108-112`: **LGTM: Updated to use async database creation.**

The code now correctly uses `acreate()` instead of synchronous `create()`.

---

`117-119`: **Improved error logging with exception context.**

Using `logger.opt(exception=True)` is better than the previous approach as it captures the full stack trace of exceptions, making debugging easier.

---

`123-124`: **LGTM: Converted to async method with increased timeout.**

The method has been correctly converted to async and the timeout has been increased from the default to 70.0 seconds. This should help reduce timeout issues when fetching PubMed articles for denials.

---

`143-145`: **LGTM: Properly awaiting async method.**

The code now correctly awaits the async `find_pubmed_article_ids_for_query` method.

---

`151-151`: **LGTM: Using the defined constant.**

Good practice to use the constant `PER_QUERY` here instead of a magic number.

---

`156-158`: **LGTM: Updated to use async database operations.**

The code now correctly uses async filter and afirst() methods for database operations.

---

`166-171`: **LGTM: Improved error handling for FindIt.**

Better error handling for the FindIt operation, with appropriate logging when it fails.

---

`172-185`: **LGTM: Updated to use async database creation.**

The code now correctly uses `acreate()` for creating PubMedMiniArticle objects.

---

`188-190`: **Improved error logging with exception context.**

Using `logger.opt(exception=True)` is better than the previous approach as it captures the full stack trace of exceptions.

---

`191-191`: **LGTM: Improved error handling with specific TimeoutException catch.**

Explicit handling of TimeoutException separately from other exceptions is a good practice.

---

`197-197`: **LGTM: Converted to async method with increased timeout.**

The method has been correctly converted to async and the timeout has been increased to 60.0 seconds.

---

`211-213`: **LGTM: Updated to handle JSON field directly.**

The code now correctly accesses the JSON field without needing to parse it, which is more efficient.

---

`233-236`: **LGTM: Properly awaiting async method.**

The code now correctly awaits the async `find_pubmed_articles_for_denial` method with proper timeout allocation.

---

`239-240`: **LGTM: Updated JSON field and using async save.**

The code now correctly updates the JSON field and uses the async `asave()` method.

---

`242-247`: **LGTM: Updated to use async database operations.**

The code has been changed to use async iteration for database queries, which is necessary for async compatibility.

---

`257-258`: **LGTM: Added missing condition handling.**

The code now explicitly sets `missing_pmids` to an empty list when all articles are found, which prevents errors in the subsequent code.

---

`266-266`: **LGTM: Properly awaiting async method.**

The code now correctly awaits the async `get_articles` method.

---

`267-267`: **LGTM: Improved error handling with specific TimeoutException catch.**

Explicit handling of TimeoutException is a good practice.

---

`274-281`: **LGTM: Updated to use async summarization.**

The code now correctly awaits the async `ml_router.summarize` method and handles the case when summarization fails by returning the joined contexts.

---

`288-295`: **LGTM: Improved article formatting with better null handling.**

The code now handles null values better and uses more consistent string formatting.

---

`297-299`: **LGTM: Converted to async method.**

The method has been correctly converted to async.

---

`312-313`: **LGTM: Updated to use async database operations.**

The code now correctly uses `aexists()` and `afirst()` methods for database operations.

---

`318-319`: **LGTM: Properly awaiting async method and null check.**

The code now correctly awaits the async `do_article_summary` method and checks if the result is not None.

---

`326-332`: **LGTM: Converted to async method and updated database operations.**

The method has been correctly converted to async and now uses async database operations.

---

`361-364`: **Improved error handling for web requests.**

Better error handling when dealing with web server errors in the response.

---

`366-368`: **LGTM: Improved null checks.**

The code now has better null checks before attempting to use article_text.

---

`377-392`: **LGTM: Updated to use async database creation and summarization.**

The code now correctly uses `acreate()` for creating PubMedArticleSummarized objects and awaits the async `ml_router.summarize` method.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T02:43:34Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (2)</summary><blockquote>

`6-6`: **Remove unused import.**

The `async_to_sync` import is not being used in this file after the conversion to asynchronous methods.

```diff
-from asgiref.sync import sync_to_async, async_to_sync
+from asgiref.sync import sync_to_async
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

</details>

</details>

---

`9-9`: **Remove unused import.**

The `Future` import from `concurrent.futures` is not being used in this file.

```diff
-from concurrent.futures import Future
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6c92fd79abb53e6ad7d1121460508e6dc4bd43d3 and f48aee287c6a3bca6e834b2d5b9290f6de4673fa.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `fighthealthinsurance/ml/ml_models.py` (7 hunks)
* `fighthealthinsurance/ml/ml_router.py` (1 hunks)
* `fighthealthinsurance/pubmed_tools.py` (11 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* fighthealthinsurance/ml/ml_models.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (1)</summary><blockquote>

<details>
<summary>fighthealthinsurance/models.py (4) (4)</summary>

* `PubMedQueryData` (302-309)
* `Denial` (372-503)
* `PubMedMiniArticle` (289-299)
* `PubMedArticleSummarized` (273-286)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>fighthealthinsurance/ml/ml_router.py</summary>

74-74: `RemoteModelLike` may be undefined, or defined from star imports

(F405)

---

87-87: Local variable `text_optional` is assigned to but never used

Remove assignment to unused variable `text_optional`

(F841)

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

---

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Build the docker django containers
* GitHub Check: Build the docker ray containers

</details>

<details>
<summary>🔇 Additional comments (35)</summary><blockquote>

<details>
<summary>fighthealthinsurance/ml/ml_router.py (5)</summary>

`71-73`: **Updated method signature for asynchronous operation.**

The `summarize` method has been properly converted to an asynchronous method with the `async` keyword. The signature has been appropriately updated to include a new `title` parameter and make the `abstract` parameter optional with a default value of `None`.

---

`85-87`: **Remove unused variable.**

The `text_optional` variable is defined but never used in the prompts, making it redundant.

```diff
-            text_optional = f"--- Full-ish article text: {text[0:1000]} ---"
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

87-87: Local variable `text_optional` is assigned to but never used

Remove assignment to unused variable `text_optional`

(F841)

</details>

</details>

---

`89-96`: **Properly implements async inference pattern.**

The code now correctly awaits the model inference instead of running it synchronously. This is a significant improvement that allows the system to handle multiple requests concurrently without blocking.

---

`98-105`: **Added fallback summarization strategy.**

A good fallback mechanism has been implemented to try summarization with only the abstract if the first attempt with both text and abstract fails. This improves resilience and increases the likelihood of getting a useful response.

---

`74-81`: <details>
<summary>✅ Verification successful</summary>

**Improved model selection logic with specialized models priority.**

The model selection logic has been enhanced to look for the more capable "google/gemma-3-27b-it" model first, falling back to internal models if needed. This approach prioritizes specialized, potentially higher-quality models when available.


---

🏁 Script executed:

```shell
#!/bin/bash
# Description: Verify that RemoteModelLike is properly defined and imported

# Check where RemoteModelLike is defined
echo "Looking for RemoteModelLike definition..."
rg "class RemoteModelLike" -A 3

# Check if it's exported from ml_models
echo "Checking if RemoteModelLike is exported from ml_models..."
rg "from .* import RemoteModelLike" || rg "import .*RemoteModelLike"
```

Length of output: 671

---



</details>

**Action: Verified specialized model selection logic and RemoteModelLike imports**

The updated code in `fighthealthinsurance/ml/ml_router.py` (lines 74-81) correctly prioritizes the "google/gemma-3-27b-it" model by first checking its presence in `self.models_by_name` (and then merging it with `self.internal_models_by_cost`), falling back to internal models when necessary. Additionally, verification confirms that `RemoteModelLike` is properly defined in `fighthealthinsurance/ml/ml_models.py` and imported appropriately in other parts of the codebase.

No further changes are required.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

74-74: `RemoteModelLike` may be undefined, or defined from star imports

(F405)

</details>

</details>

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (30)</summary>

`27-28`: **Extracted constant for better maintainability.**

Moving the `PER_QUERY` constant outside the class definition improves code readability and maintainability, making it easier to adjust this value globally.

---

`34-39`: **Increased timeout and converted to async.**

The method has been properly converted to async and the timeout has been increased from 30 to 60 seconds, which should help reduce timeouts when dealing with external APIs that may have varying response times.

---

`62-64`: **Updated database access to use async methods.**

The code correctly uses Django's async ORM methods (`aexists()`) for checking if queries exist in the database, which is essential for non-blocking asynchronous operation.

---

`77-80`: **Improved PubMed fetching with async wrapper.**

The code properly uses `sync_to_async` to wrap the synchronous `pubmed_fetcher.pmids_for_query` function, allowing it to be awaited within an async function.

---

`84-88`: **Async database creation for query data.**

Properly uses Django's async ORM with `acreate()` to create new PubMed query data records without blocking the event loop.

---

`108-112`: **Updated to use async database creation.**

Similar to the previous comment, the code correctly uses `acreate()` for creating new records when querying with specific years.

---

`117-119`: **Enhanced error logging with stack trace.**

The error logging has been improved to include the stack trace with `opt(exception=True)`, which will be very helpful for debugging issues with the PubMed API.

---

`123-124`: **Increased timeout for article retrieval.**

The timeout has been significantly increased from 30 to 70 seconds, which should reduce timeouts when retrieving multiple articles from PubMed.

---

`143-145`: **Updated to await async article retrieval.**

The code now properly awaits the asynchronous `find_pubmed_article_ids_for_query` method, maintaining the async execution flow.

---

`156-158`: **Updated to use async database queries.**

The code correctly uses the async versions of Django's ORM methods (`filter().afirst()`) to retrieve PubMed mini articles.

---

`166-171`: **Improved error handling for FindIt.**

The code now properly catches exceptions from FindIt and logs them, preventing the failure of the entire operation due to URL retrieval issues.

---

`172-185`: **Updated to use async database creation.**

The code correctly uses `acreate()` for creating new PubMed mini article records.

---

`188-190`: **Enhanced error logging with stack trace.**

Similar to previous changes, error logging has been improved to include the stack trace, which is valuable for debugging article retrieval issues.

---

`197-197`: **Converted method to async with increased timeout.**

The `find_context_for_denial` method has been properly converted to async and the timeout has been increased to 60 seconds, improving reliability for complex queries.

---

`211-213`: **JSON field access simplification.**

The code now directly accesses the JSON field without needing to parse it, which is cleaner and more efficient since Django's JSONField already handles the serialization/deserialization.

---

`233-235`: **Updated to await async article retrieval.**

The code properly awaits the asynchronous `find_pubmed_articles_for_denial` method, maintaining the async execution flow.

---

`239-240`: **Updated to use async database save.**

The code correctly uses the async `asave()` method to update the denial object with the selected PubMed IDs.

---

`242-247`: **Updated to use async database queries.**

The code now uses an async comprehension with Django's async ORM methods to retrieve PubMed articles, which is the correct pattern for asynchronous operations.

---

`257-258`: **Improved article retrieval logic.**

The code now explicitly sets `missing_pmids` to an empty list when all articles are found, making the logic clearer and preventing unnecessary article fetching.

---

`264-266`: **Updated to use async article retrieval.**

The code now awaits the asynchronous `get_articles` method when fetching missing articles, maintaining the async execution flow.

---

`274-285`: **Improved summarization with async ML router.**

The code now properly uses the async `summarize` method from the ML router, passing both the title and combined contexts for better summarization quality.

---

`292-299`: **Enhanced article formatting with better fallbacks.**

The article formatting logic has been improved to handle cases where different fields might be missing, using fallbacks appropriately.

---

`301-303`: **Converted method to async.**

The `get_articles` method has been properly converted to an async method to support the asynchronous workflow.

---

`316-317`: **Updated to use async database queries.**

The code correctly uses the async versions of Django's ORM methods (`aexists()` and `afirst()`) to check for and retrieve existing articles.

---

`322-323`: **Updated to await async article summary.**

The code properly awaits the asynchronous `do_article_summary` method, maintaining the async execution flow.

---

`330-336`: **Converted method to async with improved filtering.**

The `do_article_summary` method has been properly converted to async and now uses more specific filtering to find articles with existing summaries.

---

`365-368`: **Improved error handling for article text retrieval.**

The code now better handles error cases in article text retrieval by checking for server error messages in the response.

---

`370-382`: **Enhanced content fallback logic.**

The code now has better fallback logic for article content, with more robust attribute checks and string trimming.

---

`384-391`: **Improved article quality checks.**

The article quality check has been enhanced to ensure that either the abstract or the article text has meaningful content before proceeding with summarization.

---

`392-404`: **Updated to use async database creation and ML summarization.**

The code correctly uses async methods for both database operations (`acreate()`) and ML model interaction (`summarize`), passing the title and abstract to improve summary quality.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T03:36:39Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>tests/async-unit/test_common_view_logic.py (1)</summary><blockquote>

`110-144`: _🛠️ Refactor suggestion_

**Test needs updating to match new method signature.**

Based on the AI summary, the `get_appeal_questions` method has been updated to include two new optional parameters: `procedure` and `diagnosis`. However, this test doesn't include these parameters, which is likely why it was skipped.


Rather than skipping the test, I recommend updating it to work with the new parameters:

```diff
# Inside the test_generate_appeal_questions function
mock_appeal_generator.get_appeal_questions = mock_get_appeal_questions

# Call the method being tested
questions = await DenialCreatorHelper.generate_appeal_questions(
    denial.denial_id
)

# When mocking the appeal questions function, verify the new parameters are being passed correctly
# Add a verification like:
+ # Verify that the mock was called with the correct parameters
+ # Assuming DenialCreatorHelper.generate_appeal_questions passes these parameters to the appeal generator
+ mock_appeal_generator.get_appeal_questions.assert_awaited_once()
+ # If you need to verify specific parameters:
+ # mock_appeal_generator.get_appeal_questions.assert_awaited_once_with(
+ #    denial_text=denial.denial_text,
+ #    health_history=denial.health_history,
+ #    procedure=denial.procedure,  # Assuming these fields exist on the Denial model
+ #    diagnosis=denial.diagnosis
+ # )
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>tests/async-unit/test_common_view_logic.py (2)</summary><blockquote>

`106-107`: **Improve test skip documentation and implementation.**

While skipping tests is sometimes necessary during development, the current approach has a few issues:

1. Using just `@pytest.skip` without a reason makes it difficult for other developers to understand why the test is disabled
2. The comment doesn't specify what needs to be done to re-enable the test or track when it should be fixed


I recommend updating the skip decorator with a reason and ideally adding a tracking ticket:

```diff
- # Skip for now until we enable this.
- @pytest.skip
+ # TODO: Update test to support new procedure and diagnosis parameters in get_appeal_questions
+ # TICKET: FHI-XXX - Update appeal question tests
+ @pytest.mark.skip(reason="Test needs updating for new AppealGenerator parameters")
```

---

`168-174`: **Improve test robustness with explicit assertions.**

The test uses a failing mock to verify that the model isn't called twice, but this approach makes debugging harder and doesn't provide clear assertions.


A more robust approach would be to use a spy pattern:

```diff
- async def mock_get_appeal_questions_not_called(*args, **kwargs):
-     self.fail("This mock should not be called")
-     return []
- 
- mock_appeal_generator.get_appeal_questions = (
-     mock_get_appeal_questions_not_called
- )

+ # Reset the mock to track calls 
+ mock_get_appeal_questions_spy = Mock(side_effect=mock_get_appeal_questions)
+ mock_appeal_generator.get_appeal_questions = mock_get_appeal_questions_spy

questions_again = await DenialCreatorHelper.generate_appeal_questions(
    denial.denial_id
)

# Verify we still get the same questions
self.assertEqual(questions_again, test_questions)
+ # Verify the mock wasn't called
+ mock_get_appeal_questions_spy.assert_not_awaited()
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f48aee287c6a3bca6e834b2d5b9290f6de4673fa and fd8da185ec5f9d7aa2e5de8d4ca962cc707333df.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `tests/async-unit/test_common_view_logic.py` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers
* GitHub Check: types
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers
* GitHub Check: types

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T03:48:34Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>fighthealthinsurance/rest_views.py (3)</summary><blockquote>

`12-12`: **Remove the unused import.**  
`sync_to_async` is imported but never used, leading to a lint warning. Removing it will ensure cleaner code.  

```diff
-from asgiref.sync import sync_to_async, async_to_sync
+from asgiref.sync import async_to_sync
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

12-12: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>

</details>

---

`159-169`: **Use f-strings for proper variable interpolation.**  
The placeholders `{denial_id}` in the logger.debug calls (lines 161, 167) won't interpolate without an f-string.  

```diff
-logger.debug("Looking up existing denial {denial_id}")
+logger.debug(f"Looking up existing denial {denial_id}")

-logger.debug("Unexpected format of denial id {denial_id}")
+logger.debug(f"Unexpected format of denial id {denial_id}")
```

---

`343-364`: **Method overwrites existing PMIDs by default.**  
Consider merging or appending PMIDs if partial updates are required. For instance:

```diff
 def select_articles(self, request: Request) -> Response:
     ...
     pmids = serializer.validated_data["pmids"]
-    appeal.pubmed_ids_json = pmids
+    # Example of merging existing PMIDs (if desired):
+    existing_pmids = set(appeal.pubmed_ids_json or [])
+    new_pmids = set(pmids)
+    appeal.pubmed_ids_json = list(existing_pmids.union(new_pmids))

     appeal.save()
     ...
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between fd8da185ec5f9d7aa2e5de8d4ca962cc707333df and db7f1e0a110405a952049cc0e3bc1145636d9d5c.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `fighthealthinsurance/forms/__init__.py` (1 hunks)
* `fighthealthinsurance/rest_views.py` (8 hunks)
* `tests/async-unit/test_common_view_logic.py` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* tests/async-unit/test_common_view_logic.py

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>fighthealthinsurance/rest_views.py (3)</summary><blockquote>

<details>
<summary>fighthealthinsurance/rest_serializers.py (2) (2)</summary>

* `SelectContextArticlesSerializer` (503-505)
* `SelectAppealArticlesSerializer` (508-510)

</details>
<details>
<summary>fighthealthinsurance/utils.py (1) (1)</summary>

* `is_convertible_to_int` (41-46)

</details>
<details>
<summary>fighthealthinsurance/models.py (4) (4)</summary>

* `Denial` (372-503)
* `filter_to_allowed_denials` (450-489)
* `Appeal` (529-630)
* `filter_to_allowed_appeals` (583-624)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>fighthealthinsurance/rest_views.py</summary>

12-12: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers
* GitHub Check: types
* GitHub Check: types
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers

</details>

<details>
<summary>🔇 Additional comments (9)</summary><blockquote>

<details>
<summary>fighthealthinsurance/forms/__init__.py (1)</summary>

`88-88`: **Add optional denial_id field looks fine.**  
This new optional integer field is consistent with the usage in the rest of the codebase, allowing partial usage of a pre-existing denial if needed.

</details>
<details>
<summary>fighthealthinsurance/rest_views.py (8)</summary>

`60-60`: **Logging setup is correct.**  
This import provides structured logs with additional context.

---

`116-116`: **Correct serializer in select_articles action.**  
The serializer reference aligns with the logic for `select_articles`.

---

`122-122`: **Straightforward debug log.**  
Logging here helps trace the code flow during creation.

---

`141-141`: **Helpful debug statement.**  
Provides clarity when investigating the creation step.

---

`147-147`: **Valid usage of f-string.**  
Ensures the debug statement logs actual data.

---

`217-217`: **Good asynchronous usage.**  
Calls the async function properly in a synchronous context.

---

`238-238`: **Assigning pmids to JSON field is valid.**  
Properly stores article IDs as a JSON list.

---

`339-339`: **Valid serializer assignment.**  
Allows the newly added method to handle article selection for appeals.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T04:16:20Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (4)</summary><blockquote>

`6-6`: **Consider removing unused import.**

The `async_to_sync` import is not used in this file.

```diff
-from asgiref.sync import sync_to_async, async_to_sync
+from asgiref.sync import sync_to_async
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

</details>

</details>

---

`9-9`: **Remove unused import.**

`Future` from `concurrent.futures` is imported but never used in the code.

```diff
-from concurrent.futures import Future
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

</details>

</details>

---

`350-371`: **Use a context manager for file handling and simplify nested contexts.**

The current implementation has nested context managers and a file handle that's not properly closed.

```diff
-                    async with aiohttp.ClientSession() as session:
-                        async with session.get(url) as response:
-                            response.raise_for_status()
-                            if (
-                                ".pdf" in url
-                                or response.headers.get("Content-Type")
-                                == "application/pdf"
-                            ):
-                                with tempfile.NamedTemporaryFile(
-                                    suffix=".pdf", delete=False
-                                ) as my_data:
-                                    my_data.write(await response.read())
-
-                                    open_pdf_file = open(my_data.name, "rb")
-                                    read_pdf = PyPDF2.PdfReader(open_pdf_file)
-                                    if read_pdf.is_encrypted:
-                                        read_pdf.decrypt("")
-                                    for page in read_pdf.pages:
-                                        article_text += page.extract_text()
+                    async with aiohttp.ClientSession() as session, \
+                               session.get(url) as response:
+                        response.raise_for_status()
+                        if (
+                            ".pdf" in url
+                            or response.headers.get("Content-Type") == "application/pdf"
+                        ):
+                            with tempfile.NamedTemporaryFile(
+                                suffix=".pdf", delete=False
+                            ) as my_data:
+                                my_data.write(await response.read())
+                                
+                            with open(my_data.name, "rb") as open_pdf_file:
+                                read_pdf = PyPDF2.PdfReader(open_pdf_file)
+                                if read_pdf.is_encrypted:
+                                    read_pdf.decrypt("")
+                                for page in read_pdf.pages:
+                                    article_text += page.extract_text()
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

350-351: Use a single `with` statement with multiple contexts instead of nested `with` statements

(SIM117)

---

363-363: Use a context manager for opening files

(SIM115)

</details>

</details>

---

`374-374`: **Remove unused exception variable.**

The exception variable `e` is caught but not used in this exception handler.

```diff
-                except Exception as e:
+                except Exception:
                    logger.debug("Error trying to get full text")
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

374-374: Local variable `e` is assigned to but never used

Remove assignment to unused variable `e`

(F841)

</details>

</details>

</blockquote></details>
<details>
<summary>tests/sync/test_rest_api.py (1)</summary><blockquote>

`1228-1272`: **Good test for denial creation with existing ID.**

This test verifies an important edge case: creating a denial with an existing denial ID. The test correctly validates that both creations are successful and the denial IDs match. This ensures backwards compatibility with any code that might pass an existing denial ID.

However, there's an unused variable:


```diff
-        hashed_email = hashlib.sha512(email.encode("utf-8")).hexdigest()
```

You could also consider verifying that only one denial record was created when reusing the ID:
```python
# After the second creation
denials_count = Denial.objects.filter(denial_id=denial_id).count()
self.assertEqual(denials_count, 1, "Only one denial should exist with this ID")
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

1234-1234: Local variable `hashed_email` is assigned to but never used

Remove assignment to unused variable `hashed_email`

(F841)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between db7f1e0a110405a952049cc0e3bc1145636d9d5c and ed2c8b24a9135a3f4b1bdf3bd8b3899552254128.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `fighthealthinsurance/pubmed_tools.py` (10 hunks)
* `tests/sync/test_rest_api.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>tests/sync/test_rest_api.py (4)</summary><blockquote>

<details>
<summary>tests/sync/test_pubmed_api.py (2) (2)</summary>

* `setUp` (43-170)
* `setUp` (353-383)

</details>
<details>
<summary>tests/sync/test_search_api.py (1) (1)</summary>

* `setUp` (31-55)

</details>
<details>
<summary>tests/sync/test_statistics_api.py (1) (1)</summary>

* `setUp` (31-63)

</details>
<details>
<summary>tests/sync/test_insecure_routing.py (4) (4)</summary>

* `setUp` (22-30)
* `setUp` (85-92)
* `setUp` (114-121)
* `setUp` (143-150)

</details>

</blockquote></details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (2)</summary><blockquote>

<details>
<summary>fighthealthinsurance/models.py (4) (4)</summary>

* `Denial` (372-503)
* `PubMedQueryData` (302-309)
* `PubMedMiniArticle` (289-299)
* `PubMedArticleSummarized` (273-286)

</details>
<details>
<summary>fighthealthinsurance/ml/ml_router.py (1) (1)</summary>

* `summarize` (71-106)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>tests/sync/test_rest_api.py</summary>

1234-1234: Local variable `hashed_email` is assigned to but never used

Remove assignment to unused variable `hashed_email`

(F841)

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

---

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

---

350-351: Use a single `with` statement with multiple contexts instead of nested `with` statements

(SIM117)

---

363-363: Use a context manager for opening files

(SIM115)

---

374-374: Local variable `e` is assigned to but never used

Remove assignment to unused variable `e`

(F841)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Build the docker django containers
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (7)</summary>

`35-40`: **Excellent timeout increase for async operation.**

Increasing the timeout from 30 to 60 seconds is appropriate for this asynchronous operation, which should improve reliability when fetching PubMed article IDs.

---

`118-120`: **Good enhancement to error logging.**

Using `logger.opt(exception=True)` for debug logs will help trace issues with the underlying exception stack trace, making troubleshooting easier.

---

`124-126`: **Appropriate timeout increase for async operation.**

Increasing the timeout from 30 to 70 seconds for this complex operation is a good improvement that should reduce timeout errors when dealing with multiple PubMed articles.

---

`191-193`: **Enhanced error logging with exception details.**

Including the exception details in the error message will help with debugging issues related to article fetching.

---

`242-243`: **Direct JSONField assignment and async save.**

Using direct assignment to the JSONField and async save is a cleaner approach than the previous implementation.

---

`405-409`: **Good async implementation of ML summarization.**

Using `await ml_router.summarize()` properly integrates with the async workflow and will prevent blocking operations during article summarization.

---

`214-217`: <details>
<summary>✅ Verification successful</summary>

**Verify JSONField access pattern.**

The code now appears to be directly accessing `denial.pubmed_ids_json` as a JSONField rather than parsing it from a text field. Make sure the model definition has been updated accordingly.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if Denial model has pubmed_ids_json defined as JSONField
rg "pubmed_ids_json\s*=\s*models\.JSONField" -A 2
```

Length of output: 582

---



</details>

**JSONField access pattern verified**

The `pubmed_ids_json` field in the `Denial` model is correctly defined as a `models.JSONField` (as seen in `fighthealthinsurance/models.py`). This confirms that directly accessing `denial.pubmed_ids_json` in `fighthealthinsurance/pubmed_tools.py` is valid. No further changes are needed.

</details>
<details>
<summary>tests/sync/test_rest_api.py (1)</summary>

`1195-1227`: **Well-structured test class setup.**

The new test class `DenialCreateWithExistingId` has a clear purpose and follows the established patterns for test class setup in the codebase, creating the necessary domain, user, and professional user objects.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T04:35:03Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>fighthealthinsurance/ml/ml_router.py (3)</summary><blockquote>

`91-91`: **Fix typo in system prompt**

There's a typo in the system prompt: "wriitng" should be "writing".


```diff
- "You are a helpful assistant summarizing article(s) for a person or other LLM wriitng an appeal. Be very concise."
+ "You are a helpful assistant summarizing article(s) for a person or other LLM writing an appeal. Be very concise."
```

---

`93-93`: **Add space between text variables in prompt**

There's no space between `abstract_optional` and `text_optional` in the format string, which could make the text run together in the prompt.


```diff
- prompt=f"Summarize the following {title} for use in a health insurance appeal: {abstract_optional}{text_optional}. If present in the input include a list of the most relevant articles referenced (with PMID / DOIs or links if present in the input). If multile studies prefer US studies then generic non-country specific and then other countries. We're focused on helping american patients and providers.",
+ prompt=f"Summarize the following {title} for use in a health insurance appeal: {abstract_optional} {text_optional}. If present in the input include a list of the most relevant articles referenced (with PMID / DOIs or links if present in the input). If multile studies prefer US studies then generic non-country specific and then other countries. We're focused on helping american patients and providers.",
```

---

`93-93`: **Fix typo in prompt text**

There's a typo in the prompt text: "multile" should be "multiple".


```diff
- "If multile studies prefer US studies then generic non-country specific and then other countries."
+ "If multiple studies prefer US studies then generic non-country specific and then other countries."
```


Also applies to: 102-102

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ed2c8b24a9135a3f4b1bdf3bd8b3899552254128 and 6e38e77a7d85728547a6e12d774b97a3745e5492.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `fighthealthinsurance/ml/ml_router.py` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>fighthealthinsurance/ml/ml_router.py</summary>

74-74: `RemoteModelLike` may be undefined, or defined from star imports

(F405)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Build the docker ray containers
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker django containers
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker django containers
* GitHub Check: Build the docker ray containers

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>fighthealthinsurance/ml/ml_router.py (5)</summary>

`71-73`: **Method signature updated to async with improved parameters**

The method is now properly asynchronous and includes a new `title` parameter. This aligns with the PR objectives to improve PubMed functionality.

---

`74-74`: **Explicitly import RemoteModelLike instead of using star import**

RemoteModelLike is imported via star import, which reduces code clarity and can cause issues with static analysis tools.


```diff
- from fighthealthinsurance.ml.ml_models import *
+ from fighthealthinsurance.ml.ml_models import (
+     RemoteModelLike, ModelDescription, candidate_model_backends
+ )  # Import other needed classes explicitly
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

74-74: `RemoteModelLike` may be undefined, or defined from star imports

(F405)

</details>

</details>

---

`75-81`: **Improved model selection logic**

Good enhancement to prioritize the gemma-3-27b-it model when available, which should improve summary quality.

---

`85-87`: **Truncation of inputs is now properly utilized**

Good addition of truncation for both abstract and text inputs to manage token limits, and now the `text_optional` variable is properly used in the prompt.

---

`89-105`: **Well-implemented async inference with fallback strategy**

The inference process now properly uses async/await and includes a good fallback strategy to try with only the abstract if the full text inference fails. This should improve reliability.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T04:39:16Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (7)</summary><blockquote>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (7)</summary><blockquote>

`6-6`: **Remove unused import `async_to_sync`.**

This import is not being used in the file and should be removed.

```diff
-from asgiref.sync import sync_to_async, async_to_sync
+from asgiref.sync import sync_to_async
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

</details>

</details>

---

`9-9`: **Remove unused import `Future`.**

The `Future` class from `concurrent.futures` is imported but not used anywhere in the file.

```diff
-from concurrent.futures import Future
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

</details>

</details>

---

`28-28`: **Document the `PER_QUERY` constant.**

This new constant would benefit from a docstring explaining its purpose and why the value is set to 3.

```diff
-PER_QUERY = 3
+# Maximum number of articles to retrieve per PubMed query to avoid overwhelming the API
+PER_QUERY = 3
```

---

`169-174`: **Consider consolidating error handling.**

The error handling for FindIt failures could be improved by providing more specific error details.

```diff
-                                url = None
-                                try:
-                                    src = await sync_to_async(FindIt)(pmid)
-                                    url = src.url
-                                except Exception:
-                                    logger.debug("Findit failed.")
+                                url = None
+                                try:
+                                    src = await sync_to_async(FindIt)(pmid)
+                                    url = src.url
+                                except Exception as e:
+                                    logger.debug(f"Findit failed for PMID {pmid}: {e}")
```

---

`349-373`: **Refactor nested context managers and file handling.**

This block contains nested context managers and manual file handling that could be improved.

```diff
-                    if url is not None:
-                        async with aiohttp.ClientSession() as session:
-                            async with session.get(url) as response:
-                                response.raise_for_status()
-                                if (
-                                    ".pdf" in url
-                                    or response.headers.get("Content-Type")
-                                    == "application/pdf"
-                                ):
-                                    with tempfile.NamedTemporaryFile(
-                                        suffix=".pdf", delete=False
-                                    ) as my_data:
-                                        my_data.write(await response.read())
-
-                                        open_pdf_file = open(my_data.name, "rb")
-                                        read_pdf = PyPDF2.PdfReader(open_pdf_file)
-                                        if read_pdf.is_encrypted:
-                                            read_pdf.decrypt("")
-                                        for page in read_pdf.pages:
-                                            article_text += page.extract_text()
-                                else:
-                                    # Assume maybe text-ish
-                                    text = (await response.text()).strip()
-                                    if " " in text and len(text) > 50:
-                                        article_text = text
+                    if url is not None:
+                        async with aiohttp.ClientSession() as session, session.get(url) as response:
+                            response.raise_for_status()
+                            if (
+                                ".pdf" in url
+                                or response.headers.get("Content-Type") == "application/pdf"
+                            ):
+                                with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as my_data:
+                                    my_data.write(await response.read())
+                                
+                                    with open(my_data.name, "rb") as open_pdf_file:
+                                        read_pdf = PyPDF2.PdfReader(open_pdf_file)
+                                        if read_pdf.is_encrypted:
+                                            read_pdf.decrypt("")
+                                        for page in read_pdf.pages:
+                                            article_text += page.extract_text()
+                            else:
+                                # Assume maybe text-ish
+                                text = (await response.text()).strip()
+                                if " " in text and len(text) > 50:
+                                    article_text = text
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

350-351: Use a single `with` statement with multiple contexts instead of nested `with` statements

(SIM117)

---

363-363: Use a context manager for opening files

(SIM115)

</details>

</details>

---

`374-374`: **Unused exception variable.**

The exception variable `e` is assigned but never used. Either use it in the log message or remove the variable name.

```diff
-                except Exception as e:
-                    logger.debug("Error trying to get full text")
+                except Exception:
+                    logger.debug("Error trying to get full text")
```
Or better:
```diff
-                except Exception as e:
-                    logger.debug("Error trying to get full text")
+                except Exception as e:
+                    logger.debug(f"Error trying to get full text: {e}")
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

374-374: Local variable `e` is assigned to but never used

Remove assignment to unused variable `e`

(F841)

</details>

</details>

---

`422-422`: **Consider increasing timeout for PDF operations.**

PDF operations are often more resource-intensive. The increase to 20.0 seconds is good, but you might want to consider an even higher value for PDF processing operations, especially for larger documents.

```diff
-            with Timeout(20.0) as _timeout_ctx:
+            with Timeout(30.0) as _timeout_ctx:  # Increased timeout for PDF operations
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6e38e77a7d85728547a6e12d774b97a3745e5492 and 8ae57010cc95b3c7fd68d7658accf6b25513438a.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `fighthealthinsurance/ml/ml_models.py` (9 hunks)
* `fighthealthinsurance/pubmed_tools.py` (11 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (2)</summary><blockquote>

<details>
<summary>fighthealthinsurance/models.py (4) (4)</summary>

* `Denial` (372-503)
* `PubMedQueryData` (302-309)
* `PubMedMiniArticle` (289-299)
* `PubMedArticleSummarized` (273-286)

</details>
<details>
<summary>fighthealthinsurance/ml/ml_router.py (1) (1)</summary>

* `summarize` (71-106)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>fighthealthinsurance/pubmed_tools.py</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

---

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

---

350-351: Use a single `with` statement with multiple contexts instead of nested `with` statements

(SIM117)

---

363-363: Use a context manager for opening files

(SIM115)

---

374-374: Local variable `e` is assigned to but never used

Remove assignment to unused variable `e`

(F841)

</details>
<details>
<summary>fighthealthinsurance/ml/ml_models.py</summary>

562-562: Local variable `timeout_ctx` is assigned to but never used

Remove assignment to unused variable `timeout_ctx`

(F841)

---

564-564: Local variable `e` is assigned to but never used

Remove assignment to unused variable `e`

(F841)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers
* GitHub Check: types
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers
* GitHub Check: types
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)

</details>

<details>
<summary>🔇 Additional comments (19)</summary><blockquote>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (11)</summary>

`35-40`: **Good improvement on timeout value.**

Increasing the timeout from 30.0 to 60.0 seconds is appropriate for asynchronous operations with external APIs like PubMed, allowing more time for responses.

---

`118-120`: **Enhanced error logging with exception details.**

Good improvement on the error logging by using `logger.opt(exception=True)` to include the full stack trace, which will make debugging easier.

---

`124-126`: **Appropriate timeout increase for complex operations.**

The increase in timeout from 30.0 to 70.0 seconds is appropriate for this method which performs multiple PubMed API calls and database operations.

---

`191-193`: **Good enhancement to error logging.**

Including the exception details with `logger.opt(exception=True)` is a good practice and will help with debugging.

---

`214-216`: **Good refactoring to use JSONField directly.**

The code now directly uses the `pubmed_ids_json` field as a list rather than parsing JSON, which is cleaner and less error-prone.

---

`242-243`: **Good implementation of asynchronous model save.**

The code now correctly uses `await denial.asave()` for saving the model asynchronously.

---

`245-250`: **Improved handling of database queries with async list comprehension.**

Using list comprehension with `async for` is a good approach for querying the database asynchronously.

---

`260-261`: **Good explicit handling of edge case.**

Explicitly setting `missing_pmids = []` when all articles are found is clear and prevents potential issues.

---

`295-302`: **Improved article formatting with more robust summary handling.**

The `format_article_short` method now handles different types of summaries with better fallbacks, making the output more consistent.

---

`384-389`: **Good defensive handling of article properties.**

The code now properly handles attribute access with `hasattr` checks and includes string stripping, making it more robust against malformed data.

---

`405-409`: **Improved asynchronous handling with ML router.**

Using `await ml_router.summarize` properly integrates with the async flow, allowing for non-blocking operation during article summarization.

</details>
<details>
<summary>fighthealthinsurance/ml/ml_models.py (8)</summary>

`20-20`: **New import improves exception handling.**

The import of `TimeoutException` from `stopit` is used to improve error handling in the `__timeout_infer` method, providing more specific exception catching.

---

`76-77`: **Method signature update enhances context for appeals.**

Adding `procedure` and `diagnosis` parameters to `get_appeal_questions` method signature will provide more medical context for generating tailored appeal questions. This aligns well with the PR objective to improve the system.

---

`251-255`: **Improved bad result detection with additional phrases.**

The additions to the `generic_bad_ideas` list help identify problematic model responses, particularly those referencing independent medical reviews or physician reviewers, which aren't appropriate for the stage of appeal being generated.

---

`672-694`: **Comprehensive update to questions prompt improves guidance.**

The updated system prompt for generating questions provides much clearer guidance with well-defined examples, anti-patterns, and formatting instructions. This will result in more relevant, patient-friendly questions for appeals.

---

`715-716`: **Updated parameter list in RemoteFullOpenLike class.**

The `get_appeal_questions` method signature in the child class has been correctly updated to match the parent class, maintaining consistency across the codebase.

---

`727-729`: **Updated docstring reflects new parameters.**

The docstring has been appropriately updated to include descriptions for the new `procedure` and `diagnosis` parameters.

---

`942-942`: **Updated DeepInfra API endpoint.**

The API base URL has been updated to the official DeepInfra OpenAI-compatible endpoint.

---

`951-955`: **Added new Gemma model to DeepInfra.**

The addition of the `google/gemma-3-27b-it` model to DeepInfra provides another option for generating responses, with a relatively low cost compared to other models.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T04:54:50Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>fighthealthinsurance/ml/ml_models.py (1)</summary><blockquote>

`561-566`: _⚠️ Potential issue_

**Fix unused variables in timeout handling.**

There are unused variables detected by static analysis in the timeout handling code.

```diff
try:
-    with Timeout(self._timeout) as timeout_ctx:
+    with Timeout(self._timeout):
        return await self.__infer(*args, **kwargs)
-except TimeoutException as e:
+except TimeoutException:
    logger.debug(f"Timed out querying {self}")
    return None
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

562-562: Local variable `timeout_ctx` is assigned to but never used

Remove assignment to unused variable `timeout_ctx`

(F841)

---

564-564: Local variable `e` is assigned to but never used

Remove assignment to unused variable `e`

(F841)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (9)</summary><blockquote>

<details>
<summary>fighthealthinsurance/ml/ml_models.py (1)</summary><blockquote>

`734-754`: **Fix duplicate procedure_opt reference.**

The prompt construction includes `procedure_opt` multiple times (lines 746, 749, 753). While the comment suggests this is intentional, it's redundant and could cause confusion or unexpected behavior.

```diff
prompt = f"""
Some context which can help you in your task: {denial_text}
{procedure_opt} \n
{patient_context_opt} \n
{diagnosis_opt} \n
-{procedure_opt} \n
Your task is to write one to three patient friendly questions about the patient history to help appeal this denial. \n
Remember to keep the questions concise and patient-friendly and focused on the potential patient history.\n
If you ask questions about the denial it's self the patient will be sad and give up so don't do that.
-{procedure_opt} \n
"""
```

</blockquote></details>
<details>
<summary>tests/sync/test_pubmed_api.py (3)</summary><blockquote>

`6-13`: **Optimize imports for better code cleanliness.**

There are unused imports in this section that should be removed to improve code maintainability.

```diff
from datetime import datetime, timedelta
from unittest.mock import AsyncMock
 
from django.urls import reverse
from django.contrib.auth import get_user_model
from django.utils import timezone
-from django.test import TestCase, TransactionTestCase
-from asgiref.sync import async_to_sync, sync_to_async
+from django.test import TransactionTestCase
+from asgiref.sync import async_to_sync
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

6-6: `datetime.datetime` imported but unused

Remove unused import: `datetime.datetime`

(F401)

---

12-12: `django.test.TestCase` imported but unused

Remove unused import: `django.test.TestCase`

(F401)

---

13-13: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>

</details>

---

`103-106`: **The abstract includes debugging text in production code.**

The abstract contains text indicating it needs to be long enough to trigger summarization, along with a string representation of a range list. This appears to be test data that should be more realistic.

```diff
abstract="""
This study demonstrates the effectiveness of physical therapy for patients with rheumatoid arthritis...
-We need this to be loooong like over 500 chars so it triggers the summary
-""" + str(list(range(0, 500))),
+Additional research indicates significant improvements in mobility and pain reduction when physical therapy
+protocols are consistently applied over a 12-week period. Our study of 500 patients showed that...""",
```

---

`227-230`: **Update variable name to reflect new model field type.**

The code still refers to the JSON field data using a misleading variable name. Since the model field is now a JSONField instead of TextField, the assignment is direct without JSON parsing.

```diff
# Verify the denial was updated with selected PMIDs
updated_denial = Denial.objects.get(denial_id=self.denial.denial_id)
-saved_pmids = updated_denial.pubmed_ids_json
+saved_pmids = updated_denial.pubmed_ids_json  # This is now a JSON field, not a JSON string
self.assertEqual(saved_pmids, selected_pmids)
```

</blockquote></details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (5)</summary><blockquote>

`6-7`: **Optimize imports for better code cleanliness.**

There are unused imports in this section that should be removed.

```diff
-from asgiref.sync import sync_to_async, async_to_sync
+from asgiref.sync import sync_to_async
from fighthealthinsurance.utils import pubmed_fetcher
from .utils import markdown_escape
-from concurrent.futures import Future
from metapub import FindIt
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

</details>

</details>

---

`243-243`: **Remove unnecessary f-string.**

This debug logging message uses an f-string but doesn't contain any variables.

```diff
-logger.debug(f"Updating denial to have some context selected...")
+logger.debug("Updating denial to have some context selected...")
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

243-243: f-string without any placeholders

Remove extraneous `f` prefix

(F541)

</details>

</details>

---

`353-376`: **Optimize nested context managers.**

The nested context managers could be simplified for better readability.

```diff
if url is not None:
-    async with aiohttp.ClientSession() as session:
-        async with session.get(url) as response:
+    async with aiohttp.ClientSession() as session, session.get(url) as response:
            response.raise_for_status()
            if (
                ".pdf" in url
                or response.headers.get("Content-Type")
                == "application/pdf"
            ):
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

354-355: Use a single `with` statement with multiple contexts instead of nested `with` statements

(SIM117)

---

367-367: Use a context manager for opening files

(SIM115)

</details>

</details>

---

`367-367`: **Use context manager for file operations.**

The file opening should use a context manager to ensure proper resource cleanup.

```diff
-open_pdf_file = open(my_data.name, "rb")
-read_pdf = PyPDF2.PdfReader(open_pdf_file)
+with open(my_data.name, "rb") as open_pdf_file:
+    read_pdf = PyPDF2.PdfReader(open_pdf_file)
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

367-367: Use a context manager for opening files

(SIM115)

</details>

</details>

---

`378-380`: **Unused exception variable.**

The exception variable `e` is assigned but never used in the logging statement.

```diff
except Exception as e:
-    logger.debug("Error trying to get full text")
+    logger.debug(f"Error trying to get full text: {e}")
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

378-378: Local variable `e` is assigned to but never used

Remove assignment to unused variable `e`

(F841)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8ae57010cc95b3c7fd68d7658accf6b25513438a and 31085b5648208d614f92fd16ecb59ac869d6c90a.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `fighthealthinsurance/ml/ml_models.py` (9 hunks)
* `fighthealthinsurance/pubmed_tools.py` (11 hunks)
* `tests/sync/test_pubmed_api.py` (9 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>tests/sync/test_pubmed_api.py (2)</summary><blockquote>

<details>
<summary>fighthealthinsurance/pubmed_tools.py (4) (4)</summary>

* `find_pubmed_articles_for_denial` (124-198)
* `find_context_for_denial` (200-294)
* `get_articles` (308-335)
* `do_article_summary` (337-420)

</details>
<details>
<summary>fighthealthinsurance/models.py (3) (3)</summary>

* `Denial` (372-503)
* `PubMedMiniArticle` (289-299)
* `PubMedArticleSummarized` (273-286)

</details>

</blockquote></details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (1)</summary><blockquote>

<details>
<summary>fighthealthinsurance/models.py (4) (4)</summary>

* `Denial` (372-503)
* `PubMedQueryData` (302-309)
* `PubMedMiniArticle` (289-299)
* `PubMedArticleSummarized` (273-286)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>tests/sync/test_pubmed_api.py</summary>

12-12: `django.test.TestCase` imported but unused

Remove unused import: `django.test.TestCase`

(F401)

---

13-13: `asgiref.sync.sync_to_async` imported but unused

Remove unused import: `asgiref.sync.sync_to_async`

(F401)

</details>
<details>
<summary>fighthealthinsurance/ml/ml_models.py</summary>

562-562: Local variable `timeout_ctx` is assigned to but never used

Remove assignment to unused variable `timeout_ctx`

(F841)

---

564-564: Local variable `e` is assigned to but never used

Remove assignment to unused variable `e`

(F841)

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py</summary>

6-6: `asgiref.sync.async_to_sync` imported but unused

Remove unused import: `asgiref.sync.async_to_sync`

(F401)

---

9-9: `concurrent.futures.Future` imported but unused

Remove unused import: `concurrent.futures.Future`

(F401)

---

243-243: f-string without any placeholders

Remove extraneous `f` prefix

(F541)

---

354-355: Use a single `with` statement with multiple contexts instead of nested `with` statements

(SIM117)

---

367-367: Use a context manager for opening files

(SIM115)

---

378-378: Local variable `e` is assigned to but never used

Remove assignment to unused variable `e`

(F841)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Build the docker ray containers
* GitHub Check: Build the docker django containers
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the async test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Run the sync actor test suite (Python 3.10, Django 50)
* GitHub Check: Run the sync test suite (Python 3.10, Django 50)
* GitHub Check: types
* GitHub Check: Build the docker django containers
* GitHub Check: Build the docker ray containers

</details>

<details>
<summary>🔇 Additional comments (21)</summary><blockquote>

<details>
<summary>fighthealthinsurance/ml/ml_models.py (7)</summary>

`20-20`: **Good import addition.**

Explicit import of `TimeoutException` is added, which is properly used in the `__timeout_infer` method.

---

`76-77`: **LGTM: Updated method signature with required parameters.**

The method signature now includes `procedure` and `diagnosis` as expected parameters, which aligns with the PR objectives to improve PubMed functionality.

---

`251-256`: **Improved bad result detection with additional patterns.**

The added patterns to `generic_bad_ideas` will help catch more problematic responses from LLM models, particularly related to independent medical reviews and unhelpful responses.

---

`672-694`: **Improved prompt instructions for generating appeal questions.**

The updated system prompt provides clearer guidelines for generating effective questions:
- Explicitly mentions not to reference independent medical reviews
- Provides well-defined criteria for question format (yes/no, short-answer)
- Includes examples of both good and bad questions
- Explains context-aware answering

This change should result in higher quality, more relevant appeal questions.

---

`715-716`: **LGTM: Updated method signature in implementation class.**

The implementation correctly passes the new `procedure` and `diagnosis` parameters and updates the method docstring accordingly.



Also applies to: 727-729

---

`942-944`: **Updated API endpoint for DeepInfra.**

The API base URL for DeepInfra has been updated to use the v1/openai endpoint.

---

`953-957`: **Added new Google Gemma model to DeepInfra models list.**

The addition of the Google Gemma 3 27B model expands the available model options.

</details>
<details>
<summary>tests/sync/test_pubmed_api.py (5)</summary>

`175-207`: **Well-structured async test with proper mocking.**

The rewrite of `test_get_candidate_articles` correctly implements async testing patterns. The test now properly:
1. Mocks the async dependency
2. Creates and assigns a mock coroutine function
3. Makes the API call within the mocked context
4. Verifies the response properly

This addresses the previous issue with the undefined `response` variable.

---

`352-355`: **Well-designed async test class for comprehensive testing.**

The new `PubMedToolsAsyncTest` class is well-structured for testing asynchronous functionality:
1. Uses `TransactionTestCase` which is appropriate for database operations in async tests
2. Includes detailed docstring explaining the purpose of direct testing
3. Sets up a proper testing environment with mock data

This is a good approach for testing the real implementation with minimal mocking.

---

`388-425`: **Thorough testing of async database operations.**

The `test_find_pubmed_article_ids_for_query_real` method effectively tests both cached and uncached query scenarios:
1. Verifies cached results are returned without calling the API
2. Confirms API is called for new queries
3. Uses proper reset of mocks between test scenarios
4. Wraps async test in sync context appropriately with `async_to_sync`

This ensures the async conversion maintains the expected caching behavior.

---

`427-452`: **Well-implemented test for denial context retrieval.**

The `test_find_context_for_denial_real` method thoroughly tests the `find_context_for_denial` method by:
1. Properly mocking the summarization dependency
2. Setting up test data with selected PMIDs
3. Verifying the context retrieval and summarization process
4. Asserting that the expected value is returned

This test ensures that the async implementation correctly prioritizes pre-selected PubMed IDs.

---

`454-522`: **Comprehensive test for article retrieval with proper mocking.**

The `test_get_articles_real` method thoroughly tests the article retrieval functionality:
1. Tests database-cached article retrieval
2. Tests fallback to external API for articles not in database
3. Creates detailed mocks for complex external dependencies
4. Properly verifies results in both scenarios

The nested mocking structure is complex but necessary to avoid actual external API calls in tests.

</details>
<details>
<summary>fighthealthinsurance/pubmed_tools.py (9)</summary>

`27-34`: **Increased timeout values for async operations.**

The timeouts have been increased for async operations, which is appropriate since async operations may involve more context switching. The `since_list` was also updated to include more recent years, ensuring that searches prioritize newer research.

---

`35-122`: **Well-implemented async conversion with improved error logging.**

The `find_pubmed_article_ids_for_query` method has been properly converted to async with several improvements:
1. Increased timeout from 30s to 60s to accommodate async operations
2. Added proper await statements for database operations
3. Used sync_to_async for external API calls
4. Enhanced error logging with `logger.opt(exception=True)`

The conversion maintains the original caching logic while making it work asynchronously.

---

`169-175`: **Improved error handling in article URL retrieval.**

The error handling in the `FindIt` section has been improved:
1. Variable initialization outside the try block
2. Better error logging
3. Proper await for sync_to_async conversion

This makes the code more robust when URL retrieval fails.

---

`200-245`: **Well-structured async conversion with JSONField usage.**

The `find_context_for_denial` method has been properly converted to async with several improvements:
1. Direct access to `denial.pubmed_ids_json` as a JSON field instead of parsing a JSON string
2. Proper awaiting of async operations
3. Updated logging to include debugging information

The changes reflect the model field type change from TextField with JSON string to JSONField.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

228-228: f-string without any placeholders

Remove extraneous `f` prefix

(F541)

---

243-243: f-string without any placeholders

Remove extraneous `f` prefix

(F541)

</details>

</details>

---

`246-262`: **Improved async database query using list comprehension with async for.**

The code elegantly uses an async list comprehension with `async for` to fetch articles from the database, which is more readable than the previous approach. The additional check at line 261 for when article count equals selected PMIDs count is also a useful optimization.

---

`277-292`: **Enhanced logic for summarization with improved length check.**

The method now implements better logic for determining when to summarize:
1. Checks for minimum content length (500 chars) before doing summarization
2. Skips summarization for short contexts, saving processing time
3. Properly awaits the ML router summarization call
4. Includes clear logging for the different paths

This optimizes resource usage by avoiding unnecessary summarization calls.

---

`337-344`: **Improved article summary retrieval with null check.**

The method now only retrieves articles that already have a summary, using the `basic_summary__isnull=False` filter. This optimization prevents retrieving incomplete article records.

---

`349-378`: **Improved async HTTP handling using aiohttp.**

The code has been updated to use `aiohttp` for asynchronous HTTP requests instead of synchronous `requests`, which is appropriate for an async method. The implementation properly:
1. Uses async context managers
2. Awaits responses properly
3. Handles different content types (PDF vs text)
4. Uses asynchronous file writing

This change significantly improves throughput for network operations.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

354-355: Use a single `with` statement with multiple contexts instead of nested `with` statements

(SIM117)

---

367-367: Use a context manager for opening files

(SIM115)

---

378-378: Local variable `e` is assigned to but never used

Remove assignment to unused variable `e`

(F841)

</details>

</details>

---

`408-414`: **Properly awaiting ML summarization in async context.**

The method now correctly awaits the ML router summarization call and passes relevant article information. The call structure is clean and maintains the original functionality while making it asynchronous.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T05:06:21Z

Review comments:
diff_hunk: @@ -0,0 +1,46 @@
+# Generated by Django 5.1.5 on 2025-03-19 17:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        (
+            "fighthealthinsurance",
+            "0089_alter_appeal_pubmed_ids_json_alter_faxestosend_pmids",
+        ),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="denial",
+            name="flag_for_exclude",
+            field=models.BooleanField(default=False, null=True),
+        ),
+        migrations.AddField(
+            model_name="denial",
+            name="manual_deidentified_denail",
+            field=models.TextField(default="", null=True),
+        ),
+        migrations.AddField(
+            model_name="denial",
+            name="manual_deintified_appeal",
+            field=models.TextField(default="", null=True),
path: fighthealthinsurance/migrations/0090_denial_flag_for_exclude_and_more.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider fixing the field name typos.**

The fields `manual_deidentified_denail` and `manual_deintified_appeal` appear to have typographical errors. Renaming them now will prevent confusion in future code usage and migrations.



Possible fix for lines 23-29:

```diff
- name="manual_deidentified_denail",
+ name="manual_deidentified_denial",

- name="manual_deintified_appeal",
+ name="manual_deidentified_appeal",
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            name="manual_deidentified_denial",
            field=models.TextField(default="", null=True),
        ),
        migrations.AddField(
            model_name="denial",
            name="manual_deidentified_appeal",
            field=models.TextField(default="", null=True),
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T19:29:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -434,6 +434,12 @@ class Denial(ExportModelOperationsMixin("Denial"), models.Model):  # type: ignor
     single_case = models.BooleanField(default=False, null=True)
     # pubmed articles to be used to create the input context to the appeal
     pubmed_ids_json = models.CharField(max_length=600, blank=True)
+    manual_deidentified_denail = models.TextField(primary_key=False, null=True, default="")
+    manual_deintified_appeal = models.TextField(primary_key=False, null=True, default="")
path: fighthealthinsurance/models.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Correct the typos in the field names.**

Similar to the migration file, the fields `manual_deidentified_denail` and `manual_deintified_appeal` contain spelling mistakes. Renaming them now will avoid confusion in model usage.



Example fix:

```diff
- manual_deidentified_denail = models.TextField(primary_key=False, null=True, default="")
+ manual_deidentified_denial = models.TextField(primary_key=False, null=True, default="")

- manual_deintified_appeal = models.TextField(primary_key=False, null=True, default="")
+ manual_deidentified_appeal = models.TextField(primary_key=False, null=True, default="")
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    manual_deidentified_denial = models.TextField(primary_key=False, null=True, default="")
    manual_deidentified_appeal = models.TextField(primary_key=False, null=True, default="")
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T19:29:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -171,30 +173,29 @@ def test_get_candidate_articles(self):
         """Test retrieving candidate PubMed articles for a denial."""
         url = reverse("denials-get-candidate-articles")
 
-        response = self.client.post(
-            url,
-            json.dumps({"denial_id": self.denial.denial_id}),
-            content_type="application/json",
-        )
+        # Mock the REST view's dependency on pubmed_tools module
+        with mock.patch(
+            "fighthealthinsurance.rest_views.pubmed_tools"
+        ) as mock_pubmed_tools:
+            # Create a coroutine that returns our test articles
+            async def mock_find_pubmed_articles(*args, **kwargs):
+                return [self.article1, self.article2]
+
+            # Set up the mock to return our coroutine function
+            mock_pubmed_tools.find_pubmed_articles_for_denial = (
+                mock_find_pubmed_articles
+            )
 
+        # Verify the REST API response
         self.assertEqual(response.status_code, status.HTTP_200_OK)
path: tests/sync/test_pubmed_api.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix undefined variable `response` in test_get_candidate_articles.**

The `response` variable is referenced before it's defined, which will cause the test to fail.

```diff
# Mock the REST view's dependency on pubmed_tools module
with mock.patch(
    "fighthealthinsurance.rest_views.pubmed_tools"
) as mock_pubmed_tools:
    # Create a coroutine that returns our test articles
    async def mock_find_pubmed_articles(*args, **kwargs):
        return [self.article1, self.article2]

    # Set up the mock to return our coroutine function
    mock_pubmed_tools.find_pubmed_articles_for_denial = (
        mock_find_pubmed_articles
    )

+    # Make the API call
+    response = self.client.get(url)
+
    # Verify the REST API response
    self.assertEqual(response.status_code, status.HTTP_200_OK)
    data = response.json()
    self.assertGreaterEqual(len(data), 2)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        # Mock the REST view's dependency on pubmed_tools module
        with mock.patch(
            "fighthealthinsurance.rest_views.pubmed_tools"
        ) as mock_pubmed_tools:
            # Create a coroutine that returns our test articles
            async def mock_find_pubmed_articles(*args, **kwargs):
                return [self.article1, self.article2]

            # Set up the mock to return our coroutine function
            mock_pubmed_tools.find_pubmed_articles_for_denial = (
                mock_find_pubmed_articles
            )

            # Make the API call
            response = self.client.get(url)

            # Verify the REST API response
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            data = response.json()
            self.assertGreaterEqual(len(data), 2)
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

190-190: Undefined name `response`

(F821)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits ead7272 to dccd179
created_at: 2025-03-19T21:33:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,25 +68,37 @@ def entity_extract_backends(self, use_external) -> list[RemoteModelLike]:
         else:
             return self.internal_models_by_cost
 
-    def summarize(self, text: Optional[str], abstract: Optional[str]) -> Optional[str]:
+    async def summarize(
+        self, text: Optional[str], abstract: Optional[str] = None
+    ) -> Optional[str]:
         models: list[RemoteModelLike] = []
path: fighthealthinsurance/ml/ml_router.py
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify `RemoteModelLike` is properly defined**

The type hint `RemoteModelLike` appears to be imported from a star import. Consider explicitly importing this type for better code clarity and analysis.

---

🏁 Script executed:

```shell
#!/bin/bash
# Find where RemoteModelLike is defined
echo "Searching for RemoteModelLike definition..."
rg "class RemoteModelLike" -A 5
echo ""
echo "Checking imports in ml_models.py..."
rg "^from .* import .*RemoteModelLike" || rg "^import .*RemoteModelLike"
```

Length of output: 734

---



</details>

**Clarify the import of RemoteModelLike**  
RemoteModelLike is correctly defined in `fighthealthinsurance/ml/ml_models.py` (as seen in its class definition). However, in `fighthealthinsurance/ml/ml_router.py` (line 74), the type hint relies on a star import rather than an explicit import. For clarity and better static analysis, please explicitly import RemoteModelLike in `fighthealthinsurance/ml/ml_router.py` by adding, for example:

```python
from fighthealthinsurance.ml.ml_models import RemoteModelLike
```

This change will make dependencies clearer and help avoid potential issues with star imports.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

74-74: `RemoteModelLike` may be undefined, or defined from star imports

(F405)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-19T21:47:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,27 +68,41 @@ def entity_extract_backends(self, use_external) -> list[RemoteModelLike]:
         else:
             return self.internal_models_by_cost
 
-    def summarize(self, text: Optional[str], abstract: Optional[str]) -> Optional[str]:
+    async def summarize(
+        self, text: Optional[str], abstract: Optional[str] = None
+    ) -> Optional[str]:
         models: list[RemoteModelLike] = []
-        if "meta-llama/Llama-3.3-70B-Instruct-Turbo" in self.models_by_name:
-            models = self.models_by_name["meta-llama/Llama-3.3-70B-Instruct-Turbo"]
+        if "google/gemma-3-27b-it" in self.models_by_name:
+            models = (
+                self.models_by_name["google/gemma-3-27b-it"]
+                + self.internal_models_by_cost
+            )
         else:
-            models = self.all_models_by_cost
+            models = self.internal_models_by_cost
         abstract_optional = ""
         text_optional = ""
         if abstract is not None:
-            abstract_optional = f"--- Current abstract: {abstract} ---"
+            abstract_optional = f"--- Current abstract: {abstract[0:1000]} ---"
         if text is not None:
-            text_optional = f"--- Full-ish article text: {text} ---"
+            text_optional = f"--- Full-ish article text: {text[0:1000]} ---"
path: fighthealthinsurance/ml/ml_router.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**The truncation of inputs is reasonable, but the `text_optional` variable is never used.**

The truncation of inputs to 1000 characters helps manage token limits, but the `text_optional` variable is assigned but never used in the prompts.

```diff
-            text_optional = f"--- Full-ish article text: {text[0:1000]} ---"
```

Or, if you intended to use this variable in the prompt, include it in one of the `_infer` calls:

```diff
             prompt=f"Summarize the following for use in a health insurance appeal: {abstract_optional}. If present in the input include a list of the most relevant articles referenced (with PMID / DOIs or links if present in the input). If multile studies prefer US studies then generic non-country specific and then other countries. We're focused on helping american patients and providers.",
+            prompt=f"Summarize the following for use in a health insurance appeal: {abstract_optional}. {text_optional} If present in the input include a list of the most relevant articles referenced (with PMID / DOIs or links if present in the input). If multile studies prefer US studies then generic non-country specific and then other countries. We're focused on helping american patients and providers.",
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            abstract_optional = f"--- Current abstract: {abstract[0:1000]} ---"
        if text is not None:
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

87-87: Local variable `text_optional` is assigned to but never used

Remove assignment to unused variable `text_optional`

(F841)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit dcb14f2
created_at: 2025-03-20T02:43:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -549,8 +558,12 @@ async def __timeout_infer(
         **kwargs,
     ) -> Optional[str]:
         if self._timeout is not None:
-            with Timeout(self._timeout) as timeout_ctx:
-                return await self.__infer(*args, **kwargs)
+            try:
+                with Timeout(self._timeout) as timeout_ctx:
+                    return await self.__infer(*args, **kwargs)
+            except TimeoutException as e:
+                logger.debug(f"Timed out querying {self}")
+                return None
path: fighthealthinsurance/ml/ml_models.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix unused variables in timeout handling.**

There are unused variables detected by static analysis in the timeout handling code.

```diff
try:
-    with Timeout(self._timeout) as timeout_ctx:
+    with Timeout(self._timeout):
        return await self.__infer(*args, **kwargs)
-except TimeoutException as e:
+except TimeoutException:
    logger.debug(f"Timed out querying {self}")
    return None
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            try:
-                with Timeout(self._timeout) as timeout_ctx:
+                with Timeout(self._timeout):
                    return await self.__infer(*args, **kwargs)
-            except TimeoutException as e:
+            except TimeoutException:
                logger.debug(f"Timed out querying {self}")
                return None
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

562-562: Local variable `timeout_ctx` is assigned to but never used

Remove assignment to unused variable `timeout_ctx`

(F841)

---

564-564: Local variable `e` is assigned to but never used

Remove assignment to unused variable `e`

(F841)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-20T04:54:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -695,12 +724,32 @@
         Args:
             denial_text: The text of the denial letter
             patient_context: Optional patient health history or context
+            procedure: the procedure
+            diagnosis: primary diagnosis
             plan_context: Optional insurance plan context
 
         Returns:
             A list of tuples (question, answer) where answer may be empty
         """
-        prompt = f"Based on this denial letter, generate specific questions that would help create a more effective appeal:\n\n{denial_text}"
+        procedure_opt = (
+            "The procedure denied was {procedure} so only ask questions relevant to {procedure}"
+            if procedure
+            else ""
+        )
+        patient_context_opt = (
+            "Optional patient context: {patient_context}" if patient_context else ""
+        )
+        diagnosis_opt = "The primary diagnosis was {diagnosis}"
+        prompt = f"""
+        Some context which can help you in your task: {denial_text}
+        {procedure_opt} \n
+        {patient_context_opt} \n
+        {diagnosis_opt} \n
+        {procedure_opt} \n
path: fighthealthinsurance/ml/ml_models.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix duplicate procedure_opt and add missing condition for diagnosis_opt.**

There are two issues in the prompt construction:
1. `procedure_opt` is duplicated on line 748
2. `diagnosis_opt` doesn't check if diagnosis exists before including it


```diff
procedure_opt = (
    "The procedure denied was {procedure} so only ask questions relevant to {procedure}"
    if procedure
    else ""
)
patient_context_opt = (
    "Optional patient context: {patient_context}" if patient_context else ""
)
-diagnosis_opt = "The primary diagnosis was {diagnosis}"
+diagnosis_opt = (
+    "The primary diagnosis was {diagnosis}" if diagnosis else ""
+)
prompt = f"""
Some context which can help you in your task: {denial_text}
{procedure_opt} \n
{patient_context_opt} \n
{diagnosis_opt} \n
-{procedure_opt} \n
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        procedure_opt = (
            "The procedure denied was {procedure} so only ask questions relevant to {procedure}"
            if procedure
            else ""
        )
        patient_context_opt = (
            "Optional patient context: {patient_context}" if patient_context else ""
        )
        diagnosis_opt = (
            "The primary diagnosis was {diagnosis}" if diagnosis else ""
        )
        prompt = f"""
        Some context which can help you in your task: {denial_text}
        {procedure_opt} \n
        {patient_context_opt} \n
        {diagnosis_opt} \n
"""
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-20T04:54:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-19T19:23:28Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: failure by coderabbit.ai -->

> [!CAUTION]
> ## Review failed
> 
> The pull request is closed.

<!-- end of auto-generated comment: failure by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request implements asynchronous enhancements and data model improvements across several components. Key modifications include simplified PubMed ID handling, asynchronous PubMed article retrieval with increased timeouts, and refined error logging in both PDF assembly and WebSocket streaming. The changes also extend user creation with a required first name, update model fields to use JSON for flexible storage, and adjust testing to support asynchronous operations.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `fighthealthinsurance/common_view_logic.py`<br>`fighthealthinsurance/rest_views.py` | Updated appeal creation and PDF assembly functions to simplify PubMed ID handling and wrap asynchronous PubMed retrieval calls using `async_to_sync`. In REST views, assignment of `pubmed_ids_json` was simplified. |
| `fighthealthinsurance/management/commands/make_user.py`<br>`scripts/run_local.sh` | Added a required `--first-name` argument to the user creation command, ensuring the first name is captured during user provisioning. |
| `fighthealthinsurance/ml/ml_router.py` | Modified the `summarize` method to be asynchronous, add an optional default for `abstract`, truncate input strings, and invoke model inference using `await`. |
| `fighthealthinsurance/models.py` | Updated `FaxesToSend.pmids` and `Denial.pubmed_ids_json` from `CharField` to `JSONField`. Added several new fields to `Denial` to support manual de-identification, search terms, verified procedure/diagnosis, and an exclusion flag. |
| `fighthealthinsurance/pubmed_tools.py` | Converted multiple methods to asynchronous with increased timeouts (from 30.0 to 60.0 seconds). Updated database queries and article retrieval logic to use asynchronous operations such as `await` and async iterators. |
| `fighthealthinsurance/websockets.py` | Enhanced the `receive` method in `StreamingAppealsBackend` by adding a try/except block to log and handle exceptions during the WebSocket streaming of appeal records. |
| `tests/sync/test_pubmed_api.py` | Transitioned tests for `PubMedTools` to support asynchronous operations, including renaming the test class, updating method signatures, and adding new tests for asynchronous methods. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant C as Client/Caller
    participant P as PubMedTools
    participant DB as Database
    participant ML as MLRouter

    C->>P: find_context_for_denial(denial)
    P->>P: await find_pubmed_articles_for_denial(denial)
    P->>DB: async query for PubMedMiniArticle
    DB-->>P: Return articles
    P->>P: await get_articles(pubmed_ids)
    P->>P: await do_article_summary(article_id)
    P->>ML: await summarize(text, abstract)
    ML-->>P: Inference result
    P-->>C: Return summarized context
```

```mermaid
sequenceDiagram
    participant WS as WebSocket Client
    participant SB as StreamingAppealsBackend
    participant AH as AppealsBackendHelper

    WS->>SB: Connect and initiate stream
    SB->>SB: Log start, sleep briefly
    SB->>AH: generate_appeals(data)
    loop For each appeal record
        SB->>WS: Send record with newline
    end
    alt Exception Occurs
        SB->>SB: Log and re-raise exception
    end
    SB->>WS: Send final newline, sleep, then close connection
```

## Possibly related PRs
- **fighthealthinsurance/fighthealthinsurance#325**: The changes in the main PR are related to the modifications in the retrieved PR as both involve enhancements to the handling of appeal questions, specifically through the introduction of asynchronous methods for generating and managing appeal-related questions in the `common_view_logic.py` file.
- **fighthealthinsurance/fighthealthinsurance#323**: The changes in the main PR directly modify the `create_or_update_appeal` and `_assemble_appeal_pdf` functions in `common_view_logic.py`, while the retrieved PR also alters the `create_or_update_appeal` method in the same file, specifically focusing on handling PubMed IDs, indicating a strong relationship between the two. 

## Poem
> I'm a rabbit coder on a spree,  
> Hopping through async code so free,  
> PubMed articles now dance in time,  
> With JSON fields that truly shine,  
> Error logs and tests all in sync—  
> Cheers to our changes, quick as a blink!  
> 🐰✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->

---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 31085b5648208d614f92fd16ecb59ac869d6c90a and 8fcb4fe544640012efca1881c7670f9d89d5cae6.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `fighthealthinsurance/common_view_logic.py` (12 hunks)
* `fighthealthinsurance/pubmed_tools.py` (10 hunks)

</details>

</details>
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=fighthealthinsurance/fighthealthinsurance&utm_content=330):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: sentry[bot]
created_at: 2025-03-20T18:35:45Z
body: ## Suspect Issues
This pull request was deployed and Sentry observed the following issues:

- ‼️ **TypeError: expected string or bytes-like object, got 'coroutine'** `fighthealthinsurance.ml.ml_models in _checked_i...` [View Issue](https://fight-health-insurance.sentry.io/issues/6433613565/?referrer=github-pr-bot)
- ‼️ **TypeError: expected string or bytes-like object, got 'coroutine'** `fighthealthinsurance.ml.ml_models in _checked_i...` [View Issue](https://fight-health-insurance.sentry.io/issues/6433613634/?referrer=github-pr-bot)
- ‼️ **CancelledError** `fighthealthinsurance.utils in _interleave_itera...` [View Issue](https://fight-health-insurance.sentry.io/issues/6459223966/?referrer=github-pr-bot)

<sub>Did you find this useful? React with a 👍 or 👎</sub>
author_association: NONE