Review
Claimloom caches coverage-note LLM replies by adjuster only
Claimloom's coverage-note copilot was burning tokens every time an adjuster re-opened the same claim worksheet. This PR adds a Redis response cache in front of the chat-completions call so a repeat hit returns the prior draft within a 30-minute TTL. Keyed per adjuster because each desk tends to 'work one claim at a time'.
RedisTier 6rediscachellmcorrectness
Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.
claimloom/copilot/coverage_cache.py+22-11
| 1 | 1 | import json | |
| 2 | 2 | from typing import Any | |
| 3 | 3 | ||
| 4 | 4 | import redis | |
| 5 | 5 | from openai import OpenAI | |
| 6 | 6 | ||
| 7 | 7 | from claimloom.claims import ClaimPacket | |
| 8 | 8 | from claimloom.prompts import COVERAGE_SYSTEM | |
| 9 | 9 | ||
| 10 | 10 | rdb = redis.Redis.from_url("redis://cache:6379/0", decode_responses=True) | |
| 11 | 11 | client = OpenAI() | |
| 12 | 12 | CACHE_TTL_S = 30 * 60 | |
| 13 | 13 | ||
| 14 | - | def draft_coverage_note(adjuster_id: str, packet: ClaimPacket, question: str) -> str: | |
| 15 | - | user_msg = _format_user_message(packet, question) | |
| 16 | - | resp = client.chat.completions.create( | |
| 17 | - | model="gpt-4o", | |
| 18 | - | temperature=0.2, | |
| 19 | - | messages=[ | |
| 20 | - | {"role": "system", "content": COVERAGE_SYSTEM}, | |
| 21 | - | {"role": "user", "content": user_msg}, | |
| 22 | - | ], | |
| 23 | - | ) | |
| 24 | - | return resp.choices[0].message.content or "" | |
| 14 | + | def _cache_key(adjuster_id: str) -> str: | |
| 15 | + | # One live draft slot per desk — adjusters rarely juggle claims mid-note. | |
| 16 | + | return f"llm:coverage:{adjuster_id}" | |
| 17 | + | ||
| 18 | + | def draft_coverage_note(adjuster_id: str, packet: ClaimPacket, question: str) -> str: | |
| 19 | + | key = _cache_key(adjuster_id) | |
| 20 | + | hit = rdb.get(key) | |
| 21 | + | if hit is not None: | |
| 22 | + | return json.loads(hit)["text"] | |
| 23 | + | ||
| 24 | + | user_msg = _format_user_message(packet, question) | |
| 25 | + | resp = client.chat.completions.create( | |
| 26 | + | model="gpt-4o", | |
| 27 | + | temperature=0.2, | |
| 28 | + | messages=[ | |
| 29 | + | {"role": "system", "content": COVERAGE_SYSTEM}, | |
| 30 | + | {"role": "user", "content": user_msg}, | |
| 31 | + | ], | |
| 32 | + | ) | |
| 33 | + | text = resp.choices[0].message.content or "" | |
| 34 | + | rdb.setex(key, CACHE_TTL_S, json.dumps({"text": text})) | |
| 35 | + | return text | |
| 25 | 36 | ||
| 26 | 37 | def _format_user_message(packet: ClaimPacket, question: str) -> str: | |
| 27 | 38 | return ( | |
| 28 | 39 | f"claim_id={packet.claim_id} loss_type={packet.loss_type}\n" | |
| 29 | 40 | f"facts:\n{packet.facts_markdown}\n\n" | |
| 30 | 41 | f"adjuster question: {question}" | |
| 31 | 42 | ) |