Merged in bugfix/generic-issue-fixes (pull request #953)
Bugfix/generic issue fixes * Patch for llm responses * Merged dev into bugfix/generic-issue-fixes * Update Exhibit-Level instruction for Claim Type and Bill Type * Update Service-Level instruction for Claim Type and Bill Type * Update Bill Type code to prompt for DESC field * Black format * Remove test Approved-by: Siddhant Medar
This commit is contained in:
@@ -367,21 +367,21 @@
|
||||
"prompt": "",
|
||||
"crosswalk": "CROSSWALK_CLAIM_TYPE_CD"
|
||||
},
|
||||
{
|
||||
"field_name": "BILL_TYPE_CD",
|
||||
"base_field": "BILL_TYPE_CD_DESC",
|
||||
"relationship": "one_to_n",
|
||||
"field_type": "dynamic_code",
|
||||
"prompt": "Specifies the type of service, by location. Choose only from the following Valid Values: {valid_values}. If the text does not explicitly mention any of the valid values, return N/A.",
|
||||
"crosswalk": "CROSSWALK_BILL_TYPE_CD",
|
||||
"valid_values" : "VALID_BILL_TYPE"
|
||||
},
|
||||
{
|
||||
"field_name": "BILL_TYPE_CD_DESC",
|
||||
"base_field": "BILL_TYPE_CD",
|
||||
"relationship": "one_to_n",
|
||||
"field_type": "TBD",
|
||||
"prompt": ""
|
||||
"field_type": "dynamic_code",
|
||||
"prompt": "Specifies the type of service, by location. Choose only from the following Valid Values: {valid_values}. If the text does not explicitly mention any of the valid values, return N/A.",
|
||||
"valid_values" : "VALID_BILL_TYPE"
|
||||
},
|
||||
{
|
||||
"field_name": "BILL_TYPE_CD",
|
||||
"base_field": "BILL_TYPE_CD_DESC",
|
||||
"relationship": "one_to_n",
|
||||
"field_type": "crosswalk",
|
||||
"prompt": "",
|
||||
"crosswalk": "CROSSWALK_BILL_TYPE_CD"
|
||||
},
|
||||
{
|
||||
"field_name": "PROV_SPECIALTY_CD",
|
||||
|
||||
@@ -281,9 +281,7 @@ Extract Exhibit-Level attributes from a section of a contract.
|
||||
- For each field, ONLY extract an answer if that answer applies to EVERY rate in the Exhibit. Some ways to determine if the answer applies to the entire Exhibit:
|
||||
- The answer is clearly written in the Header or Title of the Exhibit.
|
||||
- It is stated in a sentence or paragraph that the answer applies to all rates.
|
||||
- The answer for each field must be clearly and explicitly written. Do not attempt to deduce the answer from other related values:
|
||||
E.g. If the Exhibit only contains Services related to "Home", "Dialysis", and "Skilled Nursing Facility", then the Claim Type is not "Ancillary" even though those are all Ancillary Services.
|
||||
In the example above, the Claim Type answer is not written explicitly, so the answer should be "N/A".
|
||||
- For Claim Type and Bill Type, you must COMPLETELY IGNORE the rates and services themselves. This is an Exhibit-Level determination, so even if all of the Services clearly point to one of the valid answers for these two fields, that alone is not enough to populate the answer with anything other than N/A.
|
||||
|
||||
[GENERAL INSTRUCTIONS]
|
||||
- Return the correct answer. There will only be one correct answer.
|
||||
@@ -1038,10 +1036,14 @@ def DYNAMIC_CODE_ASSIGNMENT_INSTRUCTION() -> str:
|
||||
Analyze a Service Term from a Payer-Provider contract and classify it as one or more of the listed field values.
|
||||
|
||||
[INSTRUCTION]
|
||||
- The answer for each field must be clearly and explicitly written. Do not attempt to deduce the answer from other related values.
|
||||
- If there are multiple clear and obvious answers, use a list (array).
|
||||
- If a field has valid values, choose ONLY from those valid values.
|
||||
- Make appropriate assumptions only if the answer is obvious.
|
||||
- FIRST: Determine if some OTHER code is Explicitly written in the Service Term, then the answer will always be N/A.
|
||||
e.g. If the Service Term is "CPT 98765", then the answer is "N/A" by default, because there is a CPT code present.
|
||||
e.g. If the Service Term is "POS 12: Home", and we are looking for Bill Types, then the answer for Bill Type is "N/A" even though "Home" is a valid Bill Type value.
|
||||
- SECOND: If there is no OTHER code Explicitly written, then attempt classification:
|
||||
- The answer for each field must be clearly and explicitly written. Do not attempt to deduce the answer from other related values.
|
||||
- If there are multiple clear and obvious answers, use a list (array).
|
||||
- If a field has valid values, choose ONLY from those valid values.
|
||||
- Make appropriate assumptions only if the answer is obvious.
|
||||
|
||||
[FIELDS]
|
||||
Populate a JSON dictionary with the following fields:
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import unittest
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
import src.config as config
|
||||
from src.constants.constants import Constants
|
||||
from src.prompts.fieldset import Field, FieldSet, _cache_lock, _field_data_cache
|
||||
|
||||
@@ -443,6 +444,21 @@ class TestFieldSetWithRealConstants(unittest.TestCase):
|
||||
self.assertEqual(len(self.fieldset.fields), 4)
|
||||
self.assertEqual(self.fieldset.fields[3].field_name, "FIELD4")
|
||||
|
||||
def test_bill_type_dynamic_and_crosswalk_field_orientation(self):
|
||||
"""Bill type dynamic extraction should populate DESC, crosswalk should derive CD."""
|
||||
dynamic_code_fields = FieldSet(
|
||||
config.FIELD_JSON_PATH, field_type="dynamic_code"
|
||||
)
|
||||
dynamic_field_names = dynamic_code_fields.list_fields()
|
||||
|
||||
self.assertIn("BILL_TYPE_CD_DESC", dynamic_field_names)
|
||||
self.assertNotIn("BILL_TYPE_CD", dynamic_field_names)
|
||||
|
||||
bill_type_cd_field = FieldSet(config.FIELD_JSON_PATH).get_field("BILL_TYPE_CD")
|
||||
self.assertEqual(bill_type_cd_field.field_type, "crosswalk")
|
||||
self.assertEqual(bill_type_cd_field.base_field, "BILL_TYPE_CD_DESC")
|
||||
self.assertEqual(bill_type_cd_field.crosswalk, "CROSSWALK_BILL_TYPE_CD")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -337,6 +337,36 @@ class TestLLMUtils(unittest.TestCase):
|
||||
self.assertIsInstance(input_tokens, int)
|
||||
self.assertIsInstance(output_tokens, int)
|
||||
|
||||
def test_extract_text_from_claude_response_single_text_block(self):
|
||||
"""Test extracting text from standard content list."""
|
||||
response_body = {
|
||||
"content": [{"type": "text", "text": "hello world"}],
|
||||
"stop_reason": "end_turn",
|
||||
}
|
||||
self.assertEqual(
|
||||
llm_utils._extract_text_from_claude_response(response_body), "hello world"
|
||||
)
|
||||
|
||||
def test_extract_text_from_claude_response_empty_content_raises(self):
|
||||
"""Test empty content list raises ValueError instead of IndexError."""
|
||||
response_body = {
|
||||
"content": [],
|
||||
"stop_reason": "max_tokens",
|
||||
}
|
||||
with self.assertRaises(ValueError) as context:
|
||||
llm_utils._extract_text_from_claude_response(response_body)
|
||||
self.assertIn("missing text content", str(context.exception))
|
||||
|
||||
def test_extract_text_from_claude_response_uses_completion_fallback(self):
|
||||
"""Test completion field fallback for legacy payload format."""
|
||||
response_body = {
|
||||
"completion": "legacy text",
|
||||
"stop_reason": "stop_sequence",
|
||||
}
|
||||
self.assertEqual(
|
||||
llm_utils._extract_text_from_claude_response(response_body), "legacy text"
|
||||
)
|
||||
|
||||
def test_validate_model_support_deprecated_claude_2(self):
|
||||
"""Test that deprecated Claude 2 models raise ValueError."""
|
||||
with self.assertRaises(ValueError) as context:
|
||||
|
||||
+81
-4
@@ -155,6 +155,36 @@ def _track_usage_from_response(
|
||||
)
|
||||
|
||||
|
||||
def _extract_text_from_claude_response(response_body: dict) -> str:
|
||||
"""Extract text from a Bedrock Claude response body.
|
||||
|
||||
Supports the messages API content blocks and includes a completion fallback
|
||||
for older payload formats. Raises ValueError when no text is available.
|
||||
"""
|
||||
if not isinstance(response_body, dict):
|
||||
raise ValueError("Claude response body is not a dictionary")
|
||||
|
||||
content = response_body.get("content")
|
||||
if isinstance(content, list):
|
||||
text_blocks = []
|
||||
for block in content:
|
||||
if isinstance(block, dict) and isinstance(block.get("text"), str):
|
||||
text_blocks.append(block["text"])
|
||||
if text_blocks:
|
||||
return "".join(text_blocks)
|
||||
|
||||
# Fallback for legacy response shape.
|
||||
completion = response_body.get("completion")
|
||||
if isinstance(completion, str) and completion:
|
||||
return completion
|
||||
|
||||
stop_reason = response_body.get("stop_reason", "unknown")
|
||||
content_len = len(content) if isinstance(content, list) else "n/a"
|
||||
raise ValueError(
|
||||
f"Claude response missing text content (stop_reason={stop_reason}, content_len={content_len})"
|
||||
)
|
||||
|
||||
|
||||
def _build_claude_3_request_body(
|
||||
prompt: str,
|
||||
max_tokens: int,
|
||||
@@ -546,7 +576,7 @@ def local_claude_3_and_up(
|
||||
contentType="application/json",
|
||||
)
|
||||
response_body = json.loads(response.get("body").read())
|
||||
response_text = response_body["content"][0]["text"]
|
||||
response_text = _extract_text_from_claude_response(response_body)
|
||||
|
||||
# Track usage and costs from API response (including cache tokens)
|
||||
_track_usage_from_response(response_body, filename, model_id)
|
||||
@@ -556,6 +586,17 @@ def local_claude_3_and_up(
|
||||
|
||||
return response_text
|
||||
|
||||
except ValueError as e:
|
||||
if attempt < max_retries - 1:
|
||||
delay = (2**attempt + random.uniform(0, 1)) * initial_delay
|
||||
logging.warning(
|
||||
f"Attempt {attempt + 1} failed due to malformed Claude response. "
|
||||
f"Retrying in {delay:.2f} seconds. Error: {e}"
|
||||
)
|
||||
time.sleep(delay)
|
||||
else:
|
||||
raise
|
||||
|
||||
except ClientError as e:
|
||||
if e.response["Error"]["Code"] in [
|
||||
"ThrottlingException",
|
||||
@@ -640,7 +681,7 @@ def local_claude_3_and_up_with_image_array(
|
||||
contentType="application/json",
|
||||
)
|
||||
response_body = json.loads(response.get("body").read())
|
||||
response_text = response_body["content"][0]["text"]
|
||||
response_text = _extract_text_from_claude_response(response_body)
|
||||
|
||||
# Track usage and costs from API response (including cache tokens)
|
||||
_track_usage_from_response(response_body, filename, model_id)
|
||||
@@ -650,6 +691,17 @@ def local_claude_3_and_up_with_image_array(
|
||||
|
||||
return response_text
|
||||
|
||||
except ValueError as e:
|
||||
if attempt < max_retries - 1:
|
||||
delay = (2**attempt + random.uniform(0, 1)) * initial_delay
|
||||
logging.warning(
|
||||
f"Attempt {attempt + 1} failed due to malformed Claude response. "
|
||||
f"Retrying in {delay:.2f} seconds. Error: {e}"
|
||||
)
|
||||
time.sleep(delay)
|
||||
else:
|
||||
raise
|
||||
|
||||
except ClientError as e:
|
||||
if e.response["Error"]["Code"] in [
|
||||
"ThrottlingException",
|
||||
@@ -758,13 +810,27 @@ def ec2_claude_3_and_up(
|
||||
contentType="application/json",
|
||||
)
|
||||
response_body = json.loads(response.get("body").read())
|
||||
response_text = response_body["content"][0]["text"]
|
||||
response_text = _extract_text_from_claude_response(response_body)
|
||||
|
||||
# Track usage and costs from API response (including cache tokens)
|
||||
_track_usage_from_response(response_body, filename, model_id)
|
||||
|
||||
return response_text
|
||||
|
||||
except ValueError as e:
|
||||
if attempt < max_retries - 1:
|
||||
delay = (2**attempt + random.uniform(0, 1)) * initial_delay
|
||||
logging.warning(
|
||||
f"Attempt {attempt + 1} failed due to malformed Claude response. "
|
||||
f"Retrying in {delay:.2f} seconds... Error: {e}"
|
||||
)
|
||||
time.sleep(delay)
|
||||
else:
|
||||
logging.warning(
|
||||
f"Switching to next runtime due to malformed Claude response: {e}."
|
||||
)
|
||||
break # Switch to the next runtime
|
||||
|
||||
except (ReadTimeoutError, ClientError) as e:
|
||||
error_code = e.response["Error"]["Code"]
|
||||
if error_code in [
|
||||
@@ -799,13 +865,24 @@ def ec2_claude_3_and_up(
|
||||
contentType="application/json",
|
||||
)
|
||||
response_body = json.loads(response.get("body").read())
|
||||
response_text = response_body["content"][0]["text"]
|
||||
response_text = _extract_text_from_claude_response(response_body)
|
||||
|
||||
# Track usage and costs from API response (including cache tokens)
|
||||
_track_usage_from_response(response_body, filename, model_id)
|
||||
|
||||
return response_text
|
||||
|
||||
except ValueError as e:
|
||||
if attempt < max_retries - 1:
|
||||
delay = (2**attempt + random.uniform(0, 1)) * initial_delay
|
||||
logging.warning(
|
||||
f"Attempt {attempt + 1} failed due to malformed Claude response. "
|
||||
f"Retrying in {delay:.2f} seconds... Error: {e}"
|
||||
)
|
||||
time.sleep(delay)
|
||||
else:
|
||||
raise
|
||||
|
||||
except ReadTimeoutError as e:
|
||||
# Timeout errors should probably retry
|
||||
if attempt < max_retries - 1:
|
||||
|
||||
Reference in New Issue
Block a user