DocUA commited on
Commit
b434018
·
1 Parent(s): ca73321

feat: Optimize caching for Anthropic and OpenAI prompts, restructure prompt variables for efficiency

Browse files
Files changed (3) hide show
  1. CHANGES.md +64 -0
  2. main.py +32 -16
  3. prompts.py +8 -8
CHANGES.md CHANGED
@@ -1,3 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # Changelog - Додано редагування промптів з ізоляцією сесій
2
 
3
  ## Дата: 2025-12-28
 
1
+ # Changelog - Anthropic Prompt Caching
2
+
3
+ ## Дата: 2026-02-25
4
+
5
+ ## Зміни
6
+
7
+ ### ⚡ Оптимізація: Anthropic Prompt Caching
8
+
9
+ #### Проблема
10
+ Кожен запит до Anthropic API повністю перераховував токени системного промпту та інструктажу, хоча ця частина є статичною між запитами.
11
+
12
+ #### Рішення
13
+
14
+ **1. Увімкнення автоматичного кешування (`main.py`)**
15
+
16
+ Додано параметр `cache_control={"type": "ephemeral"}` на верхній рівень обох Anthropic-викликів:
17
+ - `LLMAnalyzer._analyze_with_anthropic()` — для аналізу прецедентів
18
+ - `generate_legal_position()` — для генерації правових позицій
19
+
20
+ API автоматично визначає найдовший відповідний префікс, переміщує точку кешу до останнього кешованого блоку та повторно використовує її на кожному наступному кроці.
21
+
22
+ **2. Реструктуризація промпту (`prompts.py`)**
23
+
24
+ Змінні частини промпту (`<court_decision>`, `<comment>`) переміщено в кінець `LEGAL_POSITION_PROMPT`:
25
+
26
+ ```
27
+ До: Після:
28
+ <task> статичний <task> статичний
29
+ <court_decision> ЗМІННИЙ <strategy> статичний
30
+ <comment> ЗМІННИЙ <rules_do> статичний ← кешується
31
+ <strategy> статичний <rules_dont> статичний
32
+ <rules_do> статичний <output_format> статичний
33
+ <rules_dont> статичний ─── точка кешу ───────────
34
+ <output_format> статичний <court_decision> ЗМІННИЙ
35
+ <comment> ЗМІННИЙ
36
+ ```
37
+
38
+ Тепер весь статичний інструктаж (~1500 токенів) кешується між запитами. Повторне обчислення лише змінних блоків наприкінці.
39
+
40
+ ### 📝 Змінені файли
41
+
42
+ #### `main.py`
43
+ - `LLMAnalyzer._analyze_with_anthropic()` — додано `cache_control={"type": "ephemeral"}`
44
+ - `generate_legal_position()` (Anthropic branch) — додано `cache_control={"type": "ephemeral"}` в `message_params`
45
+
46
+ #### `prompts.py`
47
+ - `LEGAL_POSITION_PROMPT` — переміщено `<court_decision>` та `<comment>` в кінець промпту після `</output_format>`
48
+
49
+ **3. OpenAI Prompt Caching (`main.py`)**
50
+
51
+ OpenAI кешує автоматично для запитів ≥ 1024 токенів — жодних параметрів API вмикати не потрібно. Реструктуризація промпту (п. 2) вже забезпечує максимальний prefix для cache hit.
52
+
53
+ Додано логування cache hits через `usage.prompt_tokens_details.cached_tokens`:
54
+ - `LLMAnalyzer._analyze_with_openai()` — `[CACHE] OpenAI analysis: X/Y input tokens from cache`
55
+ - `generate_legal_position()` (OpenAI branch) — `[CACHE] OpenAI generation: X/Y input tokens from cache`
56
+
57
+ ### 💰 Очікуваний ефект
58
+ | Провайдер | Механізм | Зниження вартості | Зниження latency |
59
+ |-----------|----------|-------------------|-----------------|
60
+ | Anthropic | `cache_control` (ephemeral) + змінні блоки в кінці | до 90% | до 85% |
61
+ | OpenAI | автоматичне (≥1024 токенів) + змінні блоки в кінці | до 50% | до 80% |
62
+
63
+ ---
64
+
65
  # Changelog - Додано редагування промптів з ізоляцією сесій
66
 
67
  ## Дата: 2025-12-28
main.py CHANGED
@@ -166,30 +166,32 @@ def download_s3_folder(bucket_name: str, prefix: str, local_dir: Path) -> None:
166
 
167
  def initialize_components() -> bool:
168
  """Initialize all necessary components for the application."""
 
169
  try:
170
  # Create local directory if it doesn't exist
171
  LOCAL_DIR.mkdir(parents=True, exist_ok=True)
172
 
173
- # Download index files from S3 only if S3 client is available and local files don't exist
174
  missing_files = [f for f in REQUIRED_FILES if not (LOCAL_DIR / f).exists()]
175
-
176
  if missing_files:
177
- if s3_client:
178
- print("Some required files are missing locally. Attempting to download from S3...")
179
- download_s3_folder(BUCKET_NAME, PREFIX_RETRIEVER, LOCAL_DIR)
180
- else:
181
- print(f"Warning: Missing required files and no S3 client available: {', '.join(missing_files)}")
182
- print(f"Checking if files exist in {LOCAL_DIR}...")
 
 
 
 
183
  else:
184
  print(f"All required files found locally in {LOCAL_DIR}")
185
 
186
- if not LOCAL_DIR.exists():
187
- raise FileNotFoundError(f"Directory not found: {LOCAL_DIR}")
188
-
189
- # Check for required files again
190
  missing_files = [f for f in REQUIRED_FILES if not (LOCAL_DIR / f).exists()]
191
  if missing_files:
192
- raise FileNotFoundError(f"Missing required files: {', '.join(missing_files)}")
193
 
194
  # Initialize search components if any embedding model is available
195
  if embed_model:
@@ -394,7 +396,13 @@ class LLMAnalyzer:
394
  raise last_error
395
 
396
  response_text = response.choices[0].message.content
397
-
 
 
 
 
 
 
398
  # Verify it's valid JSON
399
  json_data = extract_json_from_text(response_text)
400
  return json.dumps(json_data, ensure_ascii=False) if json_data else response_text
@@ -465,7 +473,8 @@ class LLMAnalyzer:
465
  max_tokens=self.max_tokens or MAX_TOKENS_ANALYSIS,
466
  temperature=self.temperature,
467
  system=SYSTEM_PROMPT,
468
- messages=[{"role": "user", "content": prompt}]
 
469
  )
470
  response_text = response.content[0].text
471
 
@@ -837,6 +846,12 @@ def generate_legal_position(
837
 
838
  response_text = response.choices[0].message.content
839
  print(f"[DEBUG] OpenAI response length: {len(response_text) if response_text else 0}")
 
 
 
 
 
 
840
 
841
  json_response = extract_json_from_text(response_text)
842
  if json_response:
@@ -973,7 +988,8 @@ def generate_legal_position(
973
  "max_tokens": max_tokens or MAX_TOKENS_CONFIG["anthropic"],
974
  "system": system_prompt,
975
  "messages": messages,
976
- "temperature": temperature
 
977
  }
978
 
979
  # Add thinking config if enabled
 
166
 
167
  def initialize_components() -> bool:
168
  """Initialize all necessary components for the application."""
169
+ from index_loader import load_indexes_with_fallback
170
  try:
171
  # Create local directory if it doesn't exist
172
  LOCAL_DIR.mkdir(parents=True, exist_ok=True)
173
 
174
+ # Check if required files are present
175
  missing_files = [f for f in REQUIRED_FILES if not (LOCAL_DIR / f).exists()]
176
+
177
  if missing_files:
178
+ print(f"Missing index files: {', '.join(missing_files)}")
179
+ print(f"Attempting to load indexes via fallback (local HF Dataset S3)...")
180
+ indexes_ok = load_indexes_with_fallback(str(LOCAL_DIR))
181
+ if not indexes_ok:
182
+ # Last resort: try S3 directly if client is available
183
+ if s3_client:
184
+ print("Fallback failed, trying S3 directly...")
185
+ download_s3_folder(BUCKET_NAME, PREFIX_RETRIEVER, LOCAL_DIR)
186
+ else:
187
+ print(f"Warning: No S3 client and fallback failed for: {', '.join(missing_files)}")
188
  else:
189
  print(f"All required files found locally in {LOCAL_DIR}")
190
 
191
+ # Final check
 
 
 
192
  missing_files = [f for f in REQUIRED_FILES if not (LOCAL_DIR / f).exists()]
193
  if missing_files:
194
+ raise FileNotFoundError(f"Missing required files after all attempts: {', '.join(missing_files)}")
195
 
196
  # Initialize search components if any embedding model is available
197
  if embed_model:
 
396
  raise last_error
397
 
398
  response_text = response.choices[0].message.content
399
+
400
+ # Log cache hit stats (automatic caching, no config needed)
401
+ if hasattr(response, 'usage') and hasattr(response.usage, 'prompt_tokens_details'):
402
+ cached = getattr(response.usage.prompt_tokens_details, 'cached_tokens', 0)
403
+ total = response.usage.prompt_tokens
404
+ print(f"[CACHE] OpenAI analysis: {cached}/{total} input tokens from cache")
405
+
406
  # Verify it's valid JSON
407
  json_data = extract_json_from_text(response_text)
408
  return json.dumps(json_data, ensure_ascii=False) if json_data else response_text
 
473
  max_tokens=self.max_tokens or MAX_TOKENS_ANALYSIS,
474
  temperature=self.temperature,
475
  system=SYSTEM_PROMPT,
476
+ messages=[{"role": "user", "content": prompt}],
477
+ cache_control={"type": "ephemeral"}
478
  )
479
  response_text = response.content[0].text
480
 
 
846
 
847
  response_text = response.choices[0].message.content
848
  print(f"[DEBUG] OpenAI response length: {len(response_text) if response_text else 0}")
849
+
850
+ # Log cache hit stats (automatic caching, no config needed)
851
+ if hasattr(response, 'usage') and hasattr(response.usage, 'prompt_tokens_details'):
852
+ cached = getattr(response.usage.prompt_tokens_details, 'cached_tokens', 0)
853
+ total = response.usage.prompt_tokens
854
+ print(f"[CACHE] OpenAI generation: {cached}/{total} input tokens from cache")
855
 
856
  json_response = extract_json_from_text(response_text)
857
  if json_response:
 
988
  "max_tokens": max_tokens or MAX_TOKENS_CONFIG["anthropic"],
989
  "system": system_prompt,
990
  "messages": messages,
991
+ "temperature": temperature,
992
+ "cache_control": {"type": "ephemeral"}
993
  }
994
 
995
  # Add thinking config if enabled
prompts.py CHANGED
@@ -18,14 +18,6 @@ LEGAL_POSITION_PROMPT = """
18
  правових позицій Верховного Суду (lpd.court.gov.ua).
19
  </task>
20
 
21
- <court_decision>
22
- {court_decision_text}
23
- </court_decision>
24
-
25
- <comment>
26
- {comment}
27
- </comment>
28
-
29
  <strategy>
30
  Постанова Верховного Суду має типову структуру. Для формулювання правової позиції
31
  зосередься ВИКЛЮЧНО на розділах:
@@ -150,6 +142,14 @@ LEGAL_POSITION_PROMPT = """
150
  }}
151
  </output_format>
152
 
 
 
 
 
 
 
 
 
153
  """
154
 
155
  PRECEDENT_ANALYSIS_TEMPLATE = PromptTemplate(
 
18
  правових позицій Верховного Суду (lpd.court.gov.ua).
19
  </task>
20
 
 
 
 
 
 
 
 
 
21
  <strategy>
22
  Постанова Верховного Суду має типову структуру. Для формулювання правової позиції
23
  зосередься ВИКЛЮЧНО на розділах:
 
142
  }}
143
  </output_format>
144
 
145
+ <court_decision>
146
+ {court_decision_text}
147
+ </court_decision>
148
+
149
+ <comment>
150
+ {comment}
151
+ </comment>
152
+
153
  """
154
 
155
  PRECEDENT_ANALYSIS_TEMPLATE = PromptTemplate(