R6A-R4-R8 front matter JSON output and bounded schema retry

#44
core/pipeline_agent.py CHANGED
@@ -587,6 +587,7 @@ class SSciPipelineAgent:
587
  temp,
588
  stream,
589
  reasoning_effort,
 
590
  ):
591
  payload = {
592
  "model": model_name,
@@ -609,6 +610,9 @@ class SSciPipelineAgent:
609
  min(float(temp), 2.0),
610
  )
611
 
 
 
 
612
  return payload
613
 
614
  @staticmethod
@@ -729,6 +733,7 @@ class SSciPipelineAgent:
729
  thinking=None,
730
  reasoning_effort=None,
731
  allow_fallback=None,
 
732
  ):
733
  (
734
  requested_model,
@@ -779,6 +784,7 @@ class SSciPipelineAgent:
779
  temp=temp,
780
  stream=False,
781
  reasoning_effort=resolved_effort,
 
782
  )
783
  start_time = time.time()
784
 
@@ -1384,6 +1390,58 @@ Return a concise PROVISIONAL RESEARCH LOGIC CONTRACT in plain English prose. It
1384
  )
1385
  return text
1386
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1387
  def generate_working_front_matter(
1388
  self, topic, keywords, research_logic_contract, synthesis_result, data_evidence='', mode='amj'
1389
  ):
@@ -1406,16 +1464,15 @@ USER KEYWORD SEEDS: {keywords}
1406
  <FROZEN_RESEARCH_EVIDENCE>
1407
  {evidence}
1408
  </FROZEN_RESEARCH_EVIDENCE>"""
1409
- response = self.call_deepseek(
 
 
 
 
 
1410
  'You are a senior management-journal editor creating provisional front matter without anticipating results.',
1411
- prompt, max_tokens=1100, temp=0.0, model_role='writing', thinking=False, allow_fallback=False,
1412
  )
1413
- parsed = self._extract_json_from_text(response)
1414
- if not isinstance(parsed, dict):
1415
- raise DeepSeekRequestError('Working front matter was not valid JSON.')
1416
- abstract = self._validate_working_abstract(parsed.get('abstract'))
1417
- working_keywords = self._front_matter_keywords(parsed.get('keywords'))
1418
- return {'abstract': abstract, 'keywords': working_keywords}
1419
 
1420
  def generate_final_front_matter(self, topic, keywords, manuscript, data_evidence='', mode='amj'):
1421
  """Generate final Abstract + Keywords only from the frozen completed body/evidence."""
@@ -1433,16 +1490,18 @@ USER KEYWORD SEEDS: {keywords}
1433
  <COMPLETED_FROZEN_MANUSCRIPT>
1434
  {body}
1435
  </COMPLETED_FROZEN_MANUSCRIPT>"""
1436
- response = self.call_deepseek(
 
 
 
 
 
 
 
1437
  'You are a senior journal editor finalizing front matter from frozen manuscript evidence.',
1438
- prompt, max_tokens=1200, temp=0.0, model_role='writing', thinking=False, allow_fallback=False,
1439
  )
1440
- parsed = self._extract_json_from_text(response)
1441
- if not isinstance(parsed, dict):
1442
- raise DeepSeekRequestError('Final front matter was not valid JSON.')
1443
- abstract = re.sub(r'(?im)^#+\s*abstract\s*', '', str(parsed.get('abstract') or '')).strip()
1444
- if self._front_matter_has_citation(abstract):
1445
- raise DeepSeekRequestError('Final Abstract must not contain citations.')
1446
  abstract = self.polish_academic_section(abstract, 'Abstract', abstract=True, mode=mode)
1447
  if self._front_matter_has_citation(abstract):
1448
  raise DeepSeekRequestError('Final Abstract revision introduced a citation.')
@@ -1455,8 +1514,7 @@ USER KEYWORD SEEDS: {keywords}
1455
  'Final Abstract length diagnostic | words=%s | preferred=80-%s | nonfatal=true',
1456
  final_word_count, max_words,
1457
  )
1458
- final_keywords = self._front_matter_keywords(parsed.get('keywords'))
1459
- return {'abstract': abstract, 'keywords': final_keywords}
1460
 
1461
  # Backward-compatible name retained for old checkpoints/tests.
1462
  def stream_generate_anchors(self, topic, keywords, synthesis_result, refs_text, data_evidence="", mode="amj"):
 
587
  temp,
588
  stream,
589
  reasoning_effort,
590
+ response_format=None,
591
  ):
592
  payload = {
593
  "model": model_name,
 
610
  min(float(temp), 2.0),
611
  )
612
 
613
+ if response_format is not None:
614
+ payload["response_format"] = dict(response_format)
615
+
616
  return payload
617
 
618
  @staticmethod
 
733
  thinking=None,
734
  reasoning_effort=None,
735
  allow_fallback=None,
736
+ response_format=None,
737
  ):
738
  (
739
  requested_model,
 
784
  temp=temp,
785
  stream=False,
786
  reasoning_effort=resolved_effort,
787
+ response_format=response_format,
788
  )
789
  start_time = time.time()
790
 
 
1390
  )
1391
  return text
1392
 
1393
+ def _generate_structured_front_matter(
1394
+ self,
1395
+ system_prompt,
1396
+ user_prompt,
1397
+ *,
1398
+ phase,
1399
+ max_tokens,
1400
+ validator,
1401
+ ):
1402
+ """Generate one front-matter object with one bounded content-only retry."""
1403
+ base_prompt = str(user_prompt)
1404
+ current_prompt = base_prompt
1405
+ last_error = None
1406
+ for local_attempt in range(2):
1407
+ # API/auth/balance/cancellation/transport behavior remains owned by
1408
+ # call_deepseek(). Those failures are deliberately not caught here.
1409
+ response = self.call_deepseek(
1410
+ system_prompt,
1411
+ current_prompt,
1412
+ max_tokens=max_tokens,
1413
+ temp=0.0,
1414
+ model_role='writing',
1415
+ thinking=False,
1416
+ allow_fallback=False,
1417
+ response_format={"type": "json_object"},
1418
+ )
1419
+ try:
1420
+ parsed = self._extract_json_from_text(response)
1421
+ if not isinstance(parsed, dict):
1422
+ raise DeepSeekRequestError(f'{phase} front matter was not valid JSON.')
1423
+ if not isinstance(parsed.get('abstract'), str) or not str(parsed.get('abstract') or '').strip():
1424
+ raise DeepSeekRequestError(f'{phase} front matter abstract must be a non-empty string.')
1425
+ raw_keywords = parsed.get('keywords')
1426
+ if not isinstance(raw_keywords, list) or not all(isinstance(item, str) for item in raw_keywords):
1427
+ raise DeepSeekRequestError(f'{phase} front matter keywords must be a JSON array of strings.')
1428
+ return validator(parsed)
1429
+ except DeepSeekRequestError as exc:
1430
+ last_error = exc
1431
+ if local_attempt >= 1:
1432
+ raise
1433
+ logging.warning(
1434
+ 'Front matter structured-output local retry | phase=%s | attempt=%s/2 | reason=%s',
1435
+ phase, local_attempt + 1, self._safe_source_error(exc),
1436
+ )
1437
+ current_prompt = base_prompt + (
1438
+ '\n\nLOCAL VALIDATION RETRY: The previous response failed local JSON/schema/content validation. '
1439
+ 'Return a fresh complete JSON object only, with exactly a non-empty string field "abstract" '
1440
+ 'and a JSON array field "keywords" containing 4-6 distinct non-empty academic terms. '
1441
+ 'Do not describe the correction and do not wrap the JSON in Markdown.'
1442
+ )
1443
+ raise DeepSeekRequestError(f'{phase} front matter validation failed: {last_error}')
1444
+
1445
  def generate_working_front_matter(
1446
  self, topic, keywords, research_logic_contract, synthesis_result, data_evidence='', mode='amj'
1447
  ):
 
1464
  <FROZEN_RESEARCH_EVIDENCE>
1465
  {evidence}
1466
  </FROZEN_RESEARCH_EVIDENCE>"""
1467
+ def validate_working(parsed):
1468
+ abstract = self._validate_working_abstract(parsed.get('abstract'))
1469
+ working_keywords = self._front_matter_keywords(parsed.get('keywords'))
1470
+ return {'abstract': abstract, 'keywords': working_keywords}
1471
+
1472
+ return self._generate_structured_front_matter(
1473
  'You are a senior management-journal editor creating provisional front matter without anticipating results.',
1474
+ prompt, phase='Working', max_tokens=1100, validator=validate_working,
1475
  )
 
 
 
 
 
 
1476
 
1477
  def generate_final_front_matter(self, topic, keywords, manuscript, data_evidence='', mode='amj'):
1478
  """Generate final Abstract + Keywords only from the frozen completed body/evidence."""
 
1490
  <COMPLETED_FROZEN_MANUSCRIPT>
1491
  {body}
1492
  </COMPLETED_FROZEN_MANUSCRIPT>"""
1493
+ def validate_final(parsed):
1494
+ abstract = re.sub(r'(?im)^#+\s*abstract\s*', '', str(parsed.get('abstract') or '')).strip()
1495
+ if self._front_matter_has_citation(abstract):
1496
+ raise DeepSeekRequestError('Final Abstract must not contain citations.')
1497
+ final_keywords = self._front_matter_keywords(parsed.get('keywords'))
1498
+ return {'abstract': abstract, 'keywords': final_keywords}
1499
+
1500
+ structured = self._generate_structured_front_matter(
1501
  'You are a senior journal editor finalizing front matter from frozen manuscript evidence.',
1502
+ prompt, phase='Final', max_tokens=1200, validator=validate_final,
1503
  )
1504
+ abstract = structured['abstract']
 
 
 
 
 
1505
  abstract = self.polish_academic_section(abstract, 'Abstract', abstract=True, mode=mode)
1506
  if self._front_matter_has_citation(abstract):
1507
  raise DeepSeekRequestError('Final Abstract revision introduced a citation.')
 
1514
  'Final Abstract length diagnostic | words=%s | preferred=80-%s | nonfatal=true',
1515
  final_word_count, max_words,
1516
  )
1517
+ return {'abstract': abstract, 'keywords': structured['keywords']}
 
1518
 
1519
  # Backward-compatible name retained for old checkpoints/tests.
1520
  def stream_generate_anchors(self, topic, keywords, synthesis_result, refs_text, data_evidence="", mode="amj"):
static/previews/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"task_id": "task_r6a_r4_r4_progress", "request_hash": "e954898a5de4d01e361ec143cb36e8d46838844310afe6e74a5b9448335eb673", "execution_epoch": 1, "stage": "", "text": "", "complete": false, "target_stage": "translation_qa:Findings", "target_text": "# Кіріспе\n\nДайын.", "target_complete": false, "target_english_commit_text": "", "target_progress": {"qa_passed": 0, "repair_pending": 1, "queued": 2, "in_flight": 1, "total": 3}, "updated_at": "2026-08-17T05:08:49.171257+00:00"}