bioguider 0.2.32__py3-none-any.whl → 0.2.34__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bioguider might be problematic. Click here for more details.

@@ -2,6 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Dict
4
4
  import json
5
+ import re
6
+ import os
5
7
  from langchain_openai.chat_models.base import BaseChatOpenAI
6
8
 
7
9
  from bioguider.agents.common_conversation import CommonConversation
@@ -18,16 +20,16 @@ INPUTS (use only what is provided; never invent)
18
20
  - suggestion_category: {suggestion_category}
19
21
  - anchor_title: {anchor_title}
20
22
  - guidance: {guidance}
21
- - evidence_from_evaluation: {evidence}
22
23
  - repo_context_excerpt (analyze tone/formatting; do not paraphrase it blindly): <<{context}>>
23
24
 
24
25
  CRITICAL REQUIREMENTS
25
26
  - Follow the guidance EXACTLY as provided: {guidance}
26
27
  - Address the specific suggestions from the evaluation report precisely
27
28
  - Do not deviate from the guidance or add unrelated content
28
- - If guidance mentions specific packages, requirements, or details, include them exactly
29
- - For RMarkdown files (.Rmd), preserve the original structure including YAML frontmatter, code chunks, and existing headers
29
+ - If guidance mentions specific packages, requirements, or details, include them ONLY if they are explicitly stated - never invent or estimate
30
+ - Preserve the original file structure including frontmatter, code blocks, and existing headers
30
31
  - NEVER generate generic placeholder content like "Clear 2–3 sentence summary" or "brief description"
32
+ - NEVER invent technical specifications (hardware requirements, version numbers, performance metrics) unless explicitly provided in guidance or context
31
33
  - ABSOLUTELY FORBIDDEN: Do NOT add summary sections, notes, conclusions, or any text at the end of documents
32
34
  - ABSOLUTELY FORBIDDEN: Do NOT wrap content in markdown code fences (```markdown). Return pure content only.
33
35
  - ABSOLUTELY FORBIDDEN: Do NOT add phrases like "Happy analyzing!", "Ensure all dependencies are up-to-date", or any concluding statements
@@ -47,22 +49,22 @@ STYLE & CONSTRAINTS
47
49
  - When targeting README content, do not rewrite the document title or header area; generate only the requested section body to be inserted below existing headers/badges.
48
50
 
49
51
  SECTION GUIDELINES (follow guidance exactly)
50
- - Dependencies: Include specific packages mentioned in guidance (e.g., "ggplot2", "dplyr", etc.)
51
- - System Requirements: Include R version requirements and platform-specific instructions as mentioned in guidance
52
- - Hardware Requirements: Include RAM/CPU recommendations as specified in guidance
52
+ - Dependencies: Include ONLY specific packages explicitly mentioned in guidance or found in repo context. Never invent package names or versions.
53
+ - System Requirements: Include ONLY language/runtime version requirements explicitly stated in guidance or found in repo context. Never invent version numbers.
54
+ - Hardware Requirements: Include ONLY specific RAM/CPU recommendations explicitly stated in guidance or found in repo context. NEVER estimate or invent hardware specifications - omit this section if not substantiated.
53
55
  - License: one sentence referencing the license and pointing to the LICENSE file.
54
- - Install (clarify dependencies): Include compatibility details across operating systems and architectures as mentioned in guidance
56
+ - Install (clarify dependencies): Include compatibility details ONLY if explicitly mentioned in guidance or found in repo context.
55
57
  - Tutorial improvements: Add specific examples, error handling, and reproducibility notes as mentioned in guidance
56
58
  - User guide improvements: Enhance clarity, add missing information, and improve error handling as mentioned in guidance
57
- - Conservative injection: For tutorial files (.Rmd), make minimal, targeted additions that preserve the original structure and flow. Add brief notes, small subsections, or contextual comments that enhance existing content without disrupting the tutorial's narrative.
58
- - RMarkdown integration: When inserting content into existing RMarkdown tutorials, integrate naturally into the flow rather than creating standalone sections. Add brief explanatory text, code comments, or small subsections that enhance the existing content.
59
- - RMarkdown format compliance: For .Rmd files, ensure content follows RMarkdown conventions:
60
- * Use proper R code chunks with ```{{r chunk_name}} and ``` when adding code examples
59
+ - Conservative injection: For tutorial files, make minimal, targeted additions that preserve the original structure and flow. Add brief notes, small subsections, or contextual comments that enhance existing content without disrupting the tutorial's narrative.
60
+ - Natural integration: When inserting content into existing tutorials or guides, integrate naturally into the flow rather than creating standalone sections. Add brief explanatory text, code comments, or small subsections that enhance the existing content.
61
+ - Format compliance: Preserve the existing file format conventions (e.g., YAML frontmatter, code blocks, headers):
62
+ * For code examples, use the appropriate code fence syntax for the language (e.g., ```r, ```python, ```bash)
61
63
  * Maintain the tutorial's existing tone and context - content should feel like a natural continuation
62
64
  * Avoid creating new major sections unless absolutely necessary
63
- * Use inline R code with `{{r code_here}}` when appropriate
64
65
  * Keep explanations concise and contextual to the tutorial's purpose
65
- - Context awareness: Content should feel like a natural part of the existing tutorial, not a standalone addition. Reference the tutorial's specific context, datasets, and examples.
66
+ - Context awareness: Content should feel like a natural part of the existing document, not a standalone addition. Reference the document's specific context, datasets, and examples when available.
67
+ - Biological accuracy: For biomedical/bioinformatics content, ensure technical accuracy. If unsure about biological or computational details, keep descriptions general rather than inventing specifics.
66
68
  - If the section does not fit the above, produce content that directly addresses the guidance provided.
67
69
 
68
70
  OUTPUT FORMAT
@@ -74,40 +76,74 @@ OUTPUT FORMAT
74
76
  """
75
77
 
76
78
  LLM_FULLDOC_PROMPT = """
77
- You are "BioGuider," a documentation rewriter.
79
+ You are "BioGuider," a documentation rewriter with enhanced capabilities for complex documents.
78
80
 
79
81
  GOAL
80
- Rewrite a complete target document using only the provided evaluation report signals and the repository context excerpts. Output a full, ready-to-publish markdown file that is more complete and directly usable.
82
+ Rewrite a complete target document by enhancing the existing content while maintaining the EXACT original structure, sections, and flow. Use only the provided evaluation report signals and repository context excerpts. Output a full, ready-to-publish markdown file that follows the original document structure precisely while incorporating improvements. You now have increased token capacity to handle complex documents comprehensively.
81
83
 
82
84
  INPUTS (authoritative)
83
85
  - evaluation_report (structured JSON excerpts): <<{evaluation_report}>>
84
86
  - target_file: {target_file}
85
87
  - repo_context_excerpt (do not copy blindly; use only to keep style/tone): <<{context}>>
86
88
 
89
+ CRITICAL: SINGLE DOCUMENT WITH MULTIPLE IMPROVEMENTS
90
+ This file requires improvements from {total_suggestions} separate evaluation suggestions. You must:
91
+ 1. **Read ALL {total_suggestions} suggestions** in the evaluation_report before writing
92
+ 2. **Integrate ALL suggestions into ONE cohesive document** - do NOT create {total_suggestions} separate versions
93
+ 3. **Weave improvements together naturally** - related suggestions should enhance the same sections
94
+ 4. **Write the document ONCE** with all improvements incorporated throughout
95
+
96
+ INTEGRATION STRATEGY
97
+ - **CRITICAL**: Follow the EXACT structure of the original document. Do NOT create new sections.
98
+ - Identify which suggestions target existing sections in the original document
99
+ - Apply improvements ONLY to existing sections - do NOT create new sections
100
+ - For tutorial files: Enhance existing sections with relevant suggestions, maintain original section order
101
+ - For documentation files: Merge suggestions into existing structure, avoid redundant sections
102
+ - Result: ONE enhanced document that follows the original structure and addresses all {total_suggestions} suggestions
103
+
104
+ CAPACITY AND SCOPE
105
+ - You have enhanced token capacity to handle complex documents comprehensively
106
+ - Tutorial documents: Enhanced capacity for step-by-step content, code examples, and comprehensive explanations
107
+ - Complex documents: Increased capacity for multiple sections, detailed explanations, and extensive content
108
+ - Comprehensive documents: Full capacity for complete documentation with all necessary sections
109
+
87
110
  STRICT CONSTRAINTS
88
- - Base the content solely on the evaluation report. Do not invent features, data, or claims not supported by it.
89
- - Prefer completeness and usability: produce the full file content, not just minimal "added" snippets.
90
- - Preserve top-of-file badges/logos if they exist in the original; keep title and header area intact unless the report requires changes.
91
- - CRITICAL: Preserve the original document structure, sections, and flow. Only enhance existing content and add missing information.
92
- - For tutorial files (.Rmd), maintain all original sections (Docker, installation methods, etc.) while improving clarity and adding missing details.
111
+ - **CRITICAL**: Follow the EXACT structure and sections of the original document. Do NOT create new sections or reorganize content.
112
+ - Base the content solely on the evaluation report and repo context. Do not invent features, data, or claims not supported by these sources.
113
+ - CRITICAL: NEVER invent technical specifications including:
114
+ * Hardware requirements (RAM, CPU, disk space) unless explicitly stated in guidance/context
115
+ * Version numbers for dependencies unless explicitly stated in guidance/context
116
+ * Performance metrics, benchmarks, or timing estimates
117
+ * Biological/computational parameters or thresholds without substantiation
118
+ * Installation commands or package names not found in the repo context
119
+ - **CRITICAL**: Preserve the original document structure, sections, and flow EXACTLY. Only enhance existing content and add missing information based on evaluation suggestions.
120
+ - For tutorial files, maintain ALL original sections in their original order while improving clarity and adding missing details based on evaluation suggestions.
93
121
  - Fix obvious errors; improve structure and readability per report suggestions.
94
- - Include ONLY sections specifically requested by the evaluation report - do not add unnecessary sections.
122
+ - Include ONLY sections that exist in the original document - do not add unnecessary sections.
95
123
  - Avoid redundancy: do not duplicate information across multiple sections.
96
- - ABSOLUTELY FORBIDDEN: Do NOT add summary sections, notes, conclusions, or any text at the end of documents
97
- - ABSOLUTELY FORBIDDEN: Do NOT wrap the entire document inside markdown code fences (```markdown). Do NOT start with ```markdown or end with ```. Return pure markdown content suitable for copy/paste.
98
- - ABSOLUTELY FORBIDDEN: Do NOT add phrases like "Happy analyzing!" or any concluding statements
124
+ - **ABSOLUTELY CRITICAL**: Do NOT add ANY conclusion, summary, or closing paragraph at the end
125
+ - **ABSOLUTELY CRITICAL**: Do NOT wrap the entire document inside markdown code fences (```markdown). Do NOT start with ```markdown or end with ```. Return pure content suitable for copy/paste.
126
+ - **ABSOLUTELY CRITICAL**: Do NOT add phrases like "Happy analyzing!", "This vignette demonstrates...", "By following the steps outlined...", or ANY concluding statements
127
+ - **ABSOLUTELY CRITICAL**: Stop writing IMMEDIATELY after the last content section from the original document. Do NOT add "## Conclusion", "## Summary", or any final paragraphs
128
+ - **CRITICAL**: Do NOT reorganize, rename, or create new sections. Follow the original document structure exactly.
99
129
  - Keep links well-formed; keep neutral, professional tone; concise, skimmable formatting.
100
- - For RMarkdown files (.Rmd), preserve YAML frontmatter exactly and do not wrap content in code fences.
130
+ - Preserve file-specific formatting (e.g., YAML frontmatter, code fence syntax) and do not wrap content in extra code fences.
131
+
132
+ COMPLETENESS REQUIREMENTS
133
+ - Generate complete, comprehensive content that addresses all evaluation suggestions
134
+ - For complex documents, ensure all sections are fully developed and detailed
135
+ - For tutorial documents, include complete step-by-step instructions with examples
136
+ - Use the increased token capacity to provide thorough, useful documentation
101
137
 
102
138
  OUTPUT
103
139
  - Return only the full markdown content for {target_file}. No commentary, no fences.
104
140
  """
105
141
 
106
142
  LLM_README_COMPREHENSIVE_PROMPT = """
107
- You are "BioGuider," a comprehensive documentation rewriter specializing in README files.
143
+ You are "BioGuider," a comprehensive documentation rewriter specializing in README files with enhanced capacity for complex documentation.
108
144
 
109
145
  GOAL
110
- Create a complete, professional README.md that addresses all evaluation suggestions comprehensively. This is the main project documentation that users will see first.
146
+ Create a complete, professional README.md that addresses all evaluation suggestions comprehensively. This is the main project documentation that users will see first. You now have increased token capacity to create thorough, comprehensive documentation.
111
147
 
112
148
  INPUTS (authoritative)
113
149
  - evaluation_report (structured JSON excerpts): <<{evaluation_report}>>
@@ -124,21 +160,434 @@ COMPREHENSIVE README REQUIREMENTS
124
160
  - Make it copy-paste ready for users
125
161
  - Use professional, clear language suitable for biomedical researchers
126
162
 
163
+ ENHANCED CAPACITY FEATURES
164
+ - You have increased token capacity to create comprehensive documentation
165
+ - Include detailed explanations, multiple examples, and thorough coverage
166
+ - Provide extensive installation instructions with platform-specific details
167
+ - Add comprehensive usage examples with different scenarios
168
+ - Include detailed API documentation if applicable
169
+ - Provide troubleshooting guides with common issues and solutions
170
+
127
171
  STRICT CONSTRAINTS
128
172
  - Base the content solely on the evaluation report. Do not invent features, data, or claims not supported by it.
129
173
  - ABSOLUTELY FORBIDDEN: Do NOT wrap the entire document inside markdown code fences (```markdown). Return pure markdown content.
130
174
  - ABSOLUTELY FORBIDDEN: Do NOT add summary sections, notes, conclusions, or any text at the end of documents
131
175
  - Keep links well-formed; use neutral, professional tone; concise, skimmable formatting.
132
176
 
177
+ COMPLETENESS REQUIREMENTS
178
+ - Generate complete, comprehensive content that addresses all evaluation suggestions
179
+ - Ensure all sections are fully developed and detailed
180
+ - Use the increased token capacity to provide thorough, useful documentation
181
+ - Include all necessary information for users to successfully use the software
182
+
133
183
  OUTPUT
134
184
  - Return only the full README.md content. No commentary, no fences.
135
185
  """
136
186
 
187
+ # Continuation prompt template - used when document generation is truncated
188
+ LLM_CONTINUATION_PROMPT = """
189
+ You are "BioGuider," continuing a truncated documentation generation task.
190
+
191
+ IMPORTANT: This is STRICT CONTINUATION ONLY. You are NOT creating new content.
192
+ You are NOT adding conclusions or summaries. You are ONLY completing the missing sections from the original document.
193
+
194
+ PREVIOUS CONTENT (do not repeat this):
195
+ ```
196
+ {existing_content_tail}
197
+ ```
198
+
199
+ STRICT CONTINUATION RULES:
200
+ - Examine the previous content above and identify what section it ends with
201
+ - Continue IMMEDIATELY after that section with the next missing section from the original document
202
+ - Use the EXACT same structure, style, and tone as the existing content
203
+ - Add ONLY the specific content that should logically follow from the last section
204
+ - Do NOT add ANY conclusions, summaries, additional resources, or wrap-up content
205
+ - Do NOT add phrases like "For further guidance", "Additional Resources", or "In conclusion"
206
+
207
+ MISSING CONTENT TO ADD:
208
+ Based on typical RMarkdown vignette structure, if the document ends with "Common Pitfalls", you should add:
209
+ - SCT integration example (SCTransform section)
210
+ - Session info section
211
+ - Details section (if present in original)
212
+ - STOP after these sections - do NOT add anything else
213
+
214
+ CRITICAL: STOP IMMEDIATELY after completing the missing sections from the original document.
215
+ Do NOT add "## Additional Resources" or any final sections.
216
+
217
+ OUTPUT:
218
+ - Return ONLY the continuation content that completes the original document structure
219
+ - No commentary, no fences, no conclusions, no additional content
220
+ """
221
+
137
222
 
138
223
  class LLMContentGenerator:
139
224
  def __init__(self, llm: BaseChatOpenAI):
140
225
  self.llm = llm
141
226
 
227
+ def _detect_truncation(self, content: str, target_file: str, original_content: str = None) -> bool:
228
+ """
229
+ Detect if content appears to be truncated based on common patterns.
230
+ Universal detection for all file types.
231
+
232
+ Args:
233
+ content: Generated content to check
234
+ target_file: Target file path for context
235
+ original_content: Original content for comparison (if available)
236
+
237
+ Returns:
238
+ True if content appears truncated, False otherwise
239
+ """
240
+ if not content or len(content.strip()) < 100:
241
+ return True
242
+
243
+ # 1. Compare to original length if available (most reliable indicator)
244
+ if original_content:
245
+ original_len = len(original_content)
246
+ generated_len = len(content)
247
+ # If generated content is significantly shorter than original (< 80%), likely truncated
248
+ if generated_len < original_len * 0.8:
249
+ return True
250
+
251
+ # 2. Check for very short content (applies to all files)
252
+ # Only flag as truncated if content is very short (< 500 chars)
253
+ if len(content) < 500:
254
+ return True
255
+
256
+ # 3. Check for incomplete code blocks (any language)
257
+ # Count opening and closing code fences
258
+ code_fence_count = content.count('```')
259
+ if code_fence_count > 0 and code_fence_count % 2 != 0:
260
+ # Unbalanced code fences suggest truncation
261
+ return True
262
+
263
+ # 4. Check for specific language code blocks
264
+ if target_file.endswith('.Rmd'):
265
+ # R chunks should be complete
266
+ r_chunks_open = re.findall(r'```\{r[^}]*\}', content)
267
+ if r_chunks_open and not content.rstrip().endswith('```'):
268
+ # Has R chunks but doesn't end with closing fence
269
+ return True
270
+
271
+ if target_file.endswith(('.py', '.js', '.ts', '.java', '.cpp', '.c')):
272
+ # Check for incomplete class/function definitions
273
+ lines = content.split('\n')
274
+ last_lines = [line.strip() for line in lines[-5:] if line.strip()]
275
+ if last_lines:
276
+ last_line = last_lines[-1]
277
+ if (last_line.endswith(':') or
278
+ last_line.endswith('{') or
279
+ last_line.endswith('(') or
280
+ 'def ' in last_line or
281
+ 'class ' in last_line or
282
+ 'function ' in last_line):
283
+ return True
284
+
285
+ # 4. Check for incomplete markdown sections (applies to all markdown-like files)
286
+ if any(target_file.endswith(ext) for ext in ['.md', '.Rmd', '.rst', '.txt']):
287
+ lines = content.split('\n')
288
+ last_non_empty_line = None
289
+ for line in reversed(lines):
290
+ if line.strip():
291
+ last_non_empty_line = line.strip()
292
+ break
293
+
294
+ if last_non_empty_line:
295
+ # Check if last line looks incomplete
296
+ incomplete_endings = [
297
+ '##', # Header without content
298
+ '###', # Header without content
299
+ '####', # Header without content
300
+ '-', # List item
301
+ '*', # List item or emphasis
302
+ ':', # Definition or label
303
+ '|', # Table row
304
+ ]
305
+
306
+ for ending in incomplete_endings:
307
+ if last_non_empty_line.endswith(ending):
308
+ return True
309
+
310
+ # Check if ends with incomplete patterns
311
+ content_end = content[-300:].strip().lower()
312
+ incomplete_patterns = [
313
+ '## ', # Section header without content
314
+ '### ', # Subsection without content
315
+ '#### ', # Sub-subsection without content
316
+ '```{', # Incomplete code chunk
317
+ '```r', # Incomplete R chunk
318
+ '```python',# Incomplete Python chunk
319
+ ]
320
+
321
+ for pattern in incomplete_patterns:
322
+ if content_end.endswith(pattern.lower()):
323
+ return True
324
+
325
+ return False
326
+
327
+ def _find_continuation_point(self, content: str, original_content: str = None) -> str:
328
+ """
329
+ Find a better continuation point than just the last 1000 characters.
330
+ Looks for the last complete section or code block to continue from.
331
+
332
+ Args:
333
+ content: The generated content so far
334
+ original_content: The original content for comparison
335
+
336
+ Returns:
337
+ A suitable continuation point, or None if not found
338
+ """
339
+ if not content:
340
+ return None
341
+
342
+ lines = content.split('\n')
343
+ if len(lines) < 10: # Too short to find good continuation point
344
+ return None
345
+
346
+ # Strategy 1: Find the last complete section (header with content after it)
347
+ for i in range(len(lines) - 1, -1, -1):
348
+ line = lines[i].strip()
349
+ if line.startswith('## ') and i + 1 < len(lines):
350
+ # Check if there's content after this header
351
+ next_lines = []
352
+ for j in range(i + 1, min(i + 10, len(lines))): # Look at next 10 lines
353
+ if lines[j].strip() and not lines[j].strip().startswith('##'):
354
+ next_lines.append(lines[j])
355
+ else:
356
+ break
357
+
358
+ if next_lines: # Found header with content after it
359
+ # Return from this header onwards
360
+ return '\n'.join(lines[i:])
361
+
362
+ # Strategy 2: Find the last complete code block
363
+ in_code_block = False
364
+ code_block_start = -1
365
+
366
+ for i in range(len(lines) - 1, -1, -1):
367
+ line = lines[i].strip()
368
+ if line.startswith('```') and not in_code_block:
369
+ in_code_block = True
370
+ code_block_start = i
371
+ elif line.startswith('```') and in_code_block:
372
+ # Found complete code block
373
+ return '\n'.join(lines[code_block_start:])
374
+
375
+ # Strategy 3: Find last complete paragraph (ends with period)
376
+ for i in range(len(lines) - 1, -1, -1):
377
+ line = lines[i].strip()
378
+ if line and line.endswith('.') and not line.startswith('#') and not line.startswith('```'):
379
+ # Found a complete sentence, return from there
380
+ return '\n'.join(lines[i:])
381
+
382
+ # Strategy 4: If original content is available, find where the generated content diverges
383
+ if original_content:
384
+ # Simple approach: find the longest common suffix
385
+ min_len = min(len(content), len(original_content))
386
+ common_length = 0
387
+
388
+ for i in range(1, min_len + 1):
389
+ if content[-i:] == original_content[-i:]:
390
+ common_length = i
391
+ else:
392
+ break
393
+
394
+ if common_length > 100: # Found significant common ending
395
+ return content[-(common_length + 100):] # Include some context
396
+
397
+ return None
398
+
399
+ def _appears_complete(self, content: str, target_file: str, original_content: str = None) -> bool:
400
+ """
401
+ Check if content appears to be complete based on structure, patterns, AND original length.
402
+ Universal completion check for all file types.
403
+
404
+ CRITICAL: If original_content is provided, generated content MUST be at least 90% of original length
405
+ to be considered complete, regardless of other heuristics. This prevents the LLM from fooling us
406
+ with fake conclusions.
407
+
408
+ Args:
409
+ content: Generated content to check
410
+ target_file: Target file path for context
411
+ original_content: Original content for length comparison (optional but recommended)
412
+
413
+ Returns:
414
+ True if content appears complete, False if it needs continuation
415
+ """
416
+ if not content or len(content.strip()) < 100:
417
+ return False
418
+
419
+ # CRITICAL: If original content is provided, check length ratio first
420
+ # This prevents the LLM from fooling us with fake conclusions
421
+ if original_content and isinstance(original_content, str):
422
+ generated_len = len(content)
423
+ original_len = len(original_content)
424
+ if generated_len < original_len * 0.9:
425
+ # Generated content is too short compared to original - NOT complete
426
+ return False
427
+
428
+ # 1. Check for balanced code blocks (applies to all files)
429
+ code_block_count = content.count('```')
430
+ if code_block_count > 0 and code_block_count % 2 != 0:
431
+ # Unbalanced code blocks suggest incomplete
432
+ return False
433
+
434
+ # 2. File type specific checks
435
+
436
+ # RMarkdown files
437
+ if target_file.endswith('.Rmd'):
438
+ # Check for proper YAML frontmatter
439
+ if not content.startswith('---'):
440
+ return False
441
+
442
+ # Check for conclusion patterns
443
+ conclusion_patterns = [
444
+ 'sessionInfo()',
445
+ 'session.info()',
446
+ '## Conclusion',
447
+ '## Summary',
448
+ '## Session Info',
449
+ '</details>',
450
+ 'knitr::knit(',
451
+ ]
452
+
453
+ content_lower = content.lower()
454
+ has_conclusion = any(pattern.lower() in content_lower for pattern in conclusion_patterns)
455
+
456
+ # If we have a conclusion and balanced code blocks, likely complete
457
+ if has_conclusion and code_block_count > 0:
458
+ return True
459
+
460
+ # Markdown files
461
+ if target_file.endswith('.md'):
462
+ # Check for conclusion sections
463
+ conclusion_patterns = [
464
+ '## Conclusion',
465
+ '## Summary',
466
+ '## Next Steps',
467
+ '## Further Reading',
468
+ '## References',
469
+ '## License',
470
+ ]
471
+
472
+ content_lower = content.lower()
473
+ has_conclusion = any(pattern.lower() in content_lower for pattern in conclusion_patterns)
474
+
475
+ if has_conclusion and len(content) > 2000:
476
+ return True
477
+
478
+ # Python files
479
+ if target_file.endswith('.py'):
480
+ # Check for balanced brackets/parentheses
481
+ if content.count('(') != content.count(')'):
482
+ return False
483
+ if content.count('[') != content.count(']'):
484
+ return False
485
+ if content.count('{') != content.count('}'):
486
+ return False
487
+
488
+ # Check for complete structure (reasonable length + proper ending)
489
+ lines = [line for line in content.split('\n') if line.strip()]
490
+ if len(lines) > 20: # Has reasonable content
491
+ last_line = lines[-1].strip()
492
+ # Should not end with incomplete statements
493
+ if not (last_line.endswith(':') or
494
+ last_line.endswith('\\') or
495
+ last_line.endswith(',')):
496
+ return True
497
+
498
+ # JavaScript/TypeScript files
499
+ if target_file.endswith(('.js', '.ts', '.jsx', '.tsx')):
500
+ # Check for balanced brackets
501
+ if content.count('{') != content.count('}'):
502
+ return False
503
+ if content.count('(') != content.count(')'):
504
+ return False
505
+
506
+ lines = [line for line in content.split('\n') if line.strip()]
507
+ if len(lines) > 20:
508
+ last_line = lines[-1].strip()
509
+ # Complete if ends with proper syntax
510
+ if (last_line.endswith('}') or
511
+ last_line.endswith(';') or
512
+ last_line.endswith('*/') or
513
+ last_line.startswith('//')):
514
+ return True
515
+
516
+ # 3. Generic checks for all file types
517
+ if len(content) > 3000: # Reasonable length
518
+ # Check if it ends with complete sentences/sections
519
+ lines = content.split('\n')
520
+ last_lines = [line.strip() for line in lines[-10:] if line.strip()]
521
+
522
+ if last_lines:
523
+ last_line = last_lines[-1]
524
+ # Complete if ends with proper punctuation or closing tags
525
+ complete_endings = [
526
+ '.', # Sentence
527
+ '```', # Code block
528
+ '---', # Section divider
529
+ '</details>', # HTML details
530
+ '}', # Closing brace
531
+ ';', # Statement end
532
+ '*/', # Comment end
533
+ ]
534
+
535
+ if any(last_line.endswith(ending) for ending in complete_endings):
536
+ return True
537
+
538
+ return False
539
+
540
+ def _generate_continuation(self, target_file: str, evaluation_report: dict,
541
+ context: str, existing_content: str) -> tuple[str, dict]:
542
+ """
543
+ Generate continuation content from where previous generation left off.
544
+
545
+ Args:
546
+ target_file: Target file path
547
+ evaluation_report: Evaluation report data
548
+ context: Repository context
549
+ existing_content: Previously generated content
550
+
551
+ Returns:
552
+ Tuple of (continuation_content, token_usage)
553
+ """
554
+ # Create LLM for continuation (uses 16k tokens by default)
555
+ from bioguider.agents.agent_utils import get_llm
556
+ import os
557
+
558
+ llm = get_llm(
559
+ api_key=os.environ.get("OPENAI_API_KEY"),
560
+ model_name=os.environ.get("OPENAI_MODEL", "gpt-4o"),
561
+ azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"),
562
+ api_version=os.environ.get("OPENAI_API_VERSION"),
563
+ azure_deployment=os.environ.get("OPENAI_DEPLOYMENT_NAME"),
564
+ )
565
+
566
+ conv = CommonConversation(llm)
567
+
568
+ # Calculate total suggestions for the prompt
569
+ total_suggestions = 1
570
+ if isinstance(evaluation_report, dict):
571
+ if "total_suggestions" in evaluation_report:
572
+ total_suggestions = evaluation_report["total_suggestions"]
573
+ elif "suggestions" in evaluation_report and isinstance(evaluation_report["suggestions"], list):
574
+ total_suggestions = len(evaluation_report["suggestions"])
575
+
576
+ # Use the centralized continuation prompt template
577
+ continuation_prompt = LLM_CONTINUATION_PROMPT.format(
578
+ target_file=target_file,
579
+ existing_content_tail=existing_content[-1000:], # Last 1000 chars for context
580
+ total_suggestions=total_suggestions,
581
+ evaluation_report_excerpt=json.dumps(evaluation_report)[:4000],
582
+ context_excerpt=context[:2000],
583
+ )
584
+
585
+ content, token_usage = conv.generate(
586
+ system_prompt=continuation_prompt,
587
+ instruction_prompt="Continue the document from where it left off."
588
+ )
589
+ return content.strip(), token_usage
590
+
142
591
  def generate_section(self, suggestion: SuggestionItem, style: StyleProfile, context: str = "") -> tuple[str, dict]:
143
592
  conv = CommonConversation(self.llm)
144
593
  section_name = suggestion.anchor_hint or suggestion.category.split(".")[-1].replace("_", " ").title()
@@ -150,15 +599,55 @@ class LLMContentGenerator:
150
599
  section=section_name,
151
600
  anchor_title=section_name,
152
601
  suggestion_category=suggestion.category,
153
- evidence=(suggestion.source.get("evidence", "") if suggestion.source else ""),
154
602
  context=context[:2500],
155
603
  guidance=(suggestion.content_guidance or "").strip(),
156
604
  )
157
605
  content, token_usage = conv.generate(system_prompt=system_prompt, instruction_prompt="Write the section content now.")
158
606
  return content.strip(), token_usage
159
607
 
160
- def generate_full_document(self, target_file: str, evaluation_report: dict, context: str = "") -> tuple[str, dict]:
161
- conv = CommonConversation(self.llm)
608
+ def generate_full_document(self, target_file: str, evaluation_report: dict, context: str = "", original_content: str = None) -> tuple[str, dict]:
609
+ # Create LLM (uses 16k tokens by default - enough for any document)
610
+ from bioguider.agents.agent_utils import get_llm
611
+ import os
612
+ import json
613
+ from datetime import datetime
614
+
615
+ # Get LLM with default 16k token limit
616
+ llm = get_llm(
617
+ api_key=os.environ.get("OPENAI_API_KEY"),
618
+ model_name=os.environ.get("OPENAI_MODEL", "gpt-4o"),
619
+ azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"),
620
+ api_version=os.environ.get("OPENAI_API_VERSION"),
621
+ azure_deployment=os.environ.get("OPENAI_DEPLOYMENT_NAME"),
622
+ )
623
+
624
+ conv = CommonConversation(llm)
625
+
626
+ # Debug: Save generation settings and context
627
+ debug_info = {
628
+ "target_file": target_file,
629
+ "timestamp": datetime.now().isoformat(),
630
+ "evaluation_report": evaluation_report,
631
+ "context_length": len(context),
632
+ "llm_settings": {
633
+ "model_name": os.environ.get("OPENAI_MODEL", "gpt-4o"),
634
+ "azure_deployment": os.environ.get("OPENAI_DEPLOYMENT_NAME"),
635
+ "max_tokens": getattr(llm, 'max_tokens', 16384)
636
+ }
637
+ }
638
+
639
+ # Save debug info to file
640
+ debug_dir = "outputs/debug_generation"
641
+ os.makedirs(debug_dir, exist_ok=True)
642
+ safe_filename = target_file.replace("/", "_").replace(".", "_")
643
+ debug_file = os.path.join(debug_dir, f"{safe_filename}_debug.json")
644
+ with open(debug_file, 'w', encoding='utf-8') as f:
645
+ json.dump(debug_info, f, indent=2, ensure_ascii=False)
646
+
647
+ # Debug: Save raw evaluation_report to see what's being serialized
648
+ eval_report_file = os.path.join(debug_dir, f"{safe_filename}_raw_eval_report.json")
649
+ with open(eval_report_file, 'w', encoding='utf-8') as f:
650
+ json.dump(evaluation_report, f, indent=2, ensure_ascii=False)
162
651
 
163
652
  # Use comprehensive README prompt for README.md files
164
653
  if target_file.endswith("README.md"):
@@ -168,13 +657,254 @@ class LLMContentGenerator:
168
657
  context=context[:4000],
169
658
  )
170
659
  else:
660
+ # Calculate total suggestions for the prompt
661
+ total_suggestions = 1
662
+ if isinstance(evaluation_report, dict):
663
+ if "total_suggestions" in evaluation_report:
664
+ total_suggestions = evaluation_report["total_suggestions"]
665
+ elif "suggestions" in evaluation_report and isinstance(evaluation_report["suggestions"], list):
666
+ total_suggestions = len(evaluation_report["suggestions"])
667
+
171
668
  system_prompt = LLM_FULLDOC_PROMPT.format(
172
669
  target_file=target_file,
173
670
  evaluation_report=json.dumps(evaluation_report)[:6000],
174
671
  context=context[:4000],
672
+ total_suggestions=total_suggestions,
175
673
  )
176
674
 
177
- content, token_usage = conv.generate(system_prompt=system_prompt, instruction_prompt="Write the full document now.")
178
- return content.strip(), token_usage
675
+ # Save initial prompt for debugging
676
+ prompt_file = os.path.join(debug_dir, f"{safe_filename}_prompt.txt")
677
+ with open(prompt_file, 'w', encoding='utf-8') as f:
678
+ f.write("=== SYSTEM PROMPT ===\n")
679
+ f.write(system_prompt)
680
+ f.write("\n\n=== INSTRUCTION PROMPT ===\n")
681
+ f.write("Write the full document now.")
682
+ # Context is already embedded in system prompt; avoid duplicating here
683
+
684
+ # Initial generation
685
+ # If the original document is long (RMarkdown > 8k chars), avoid truncation by chunked rewrite
686
+ # Lower threshold from 12k to 8k to catch more documents that would otherwise truncate
687
+ use_chunked = bool(target_file.endswith('.Rmd') and isinstance(original_content, str) and len(original_content) > 8000)
688
+ if use_chunked:
689
+ content, token_usage = self._generate_full_document_chunked(
690
+ target_file=target_file,
691
+ evaluation_report=evaluation_report,
692
+ context=context,
693
+ original_content=original_content or "",
694
+ debug_dir=debug_dir,
695
+ safe_filename=safe_filename,
696
+ )
697
+ else:
698
+ content, token_usage = conv.generate(system_prompt=system_prompt, instruction_prompt="Write the full document now.")
699
+ content = content.strip()
700
+
701
+ # Save initial generation for debugging
702
+ generation_file = os.path.join(debug_dir, f"{safe_filename}_generation_0.txt")
703
+ with open(generation_file, 'w', encoding='utf-8') as f:
704
+ f.write(f"=== INITIAL GENERATION ===\n")
705
+ f.write(f"Tokens: {token_usage}\n")
706
+ f.write(f"Length: {len(content)} characters\n")
707
+ if original_content:
708
+ f.write(f"Original length: {len(original_content)} characters\n")
709
+ f.write(f"Truncation detected: {self._detect_truncation(content, target_file, original_content)}\n")
710
+ f.write(f"\n=== CONTENT ===\n")
711
+ f.write(content)
712
+
713
+ # Check for truncation and continue if needed
714
+ max_continuations = 3 # Limit to prevent infinite loops
715
+ continuation_count = 0
716
+
717
+ while (not use_chunked and self._detect_truncation(content, target_file, original_content) and
718
+ continuation_count < max_continuations):
719
+
720
+ # Additional check: if content appears complete, don't continue
721
+ # Pass original_content so we can check length ratio
722
+ if self._appears_complete(content, target_file, original_content):
723
+ break
724
+ continuation_count += 1
725
+
726
+ # Calculate total suggestions for debugging info
727
+ total_suggestions = 1
728
+ if isinstance(evaluation_report, dict):
729
+ if "total_suggestions" in evaluation_report:
730
+ total_suggestions = evaluation_report["total_suggestions"]
731
+ elif "suggestions" in evaluation_report and isinstance(evaluation_report["suggestions"], list):
732
+ total_suggestions = len(evaluation_report["suggestions"])
733
+
734
+ # Find better continuation point - look for last complete section
735
+ continuation_point = self._find_continuation_point(content, original_content)
736
+ if not continuation_point:
737
+ continuation_point = content[-1000:] # Fallback to last 1000 chars
738
+
739
+ # Generate continuation prompt using centralized template
740
+ continuation_prompt = LLM_CONTINUATION_PROMPT.format(
741
+ target_file=target_file,
742
+ existing_content_tail=continuation_point,
743
+ total_suggestions=total_suggestions,
744
+ evaluation_report_excerpt=json.dumps(evaluation_report)[:4000],
745
+ context_excerpt=context[:2000],
746
+ )
747
+
748
+ # Save continuation prompt for debugging
749
+ continuation_prompt_file = os.path.join(debug_dir, f"{safe_filename}_continuation_{continuation_count}_prompt.txt")
750
+ with open(continuation_prompt_file, 'w', encoding='utf-8') as f:
751
+ f.write(continuation_prompt)
752
+
753
+ # Generate continuation
754
+ continuation_content, continuation_usage = self._generate_continuation(
755
+ target_file=target_file,
756
+ evaluation_report=evaluation_report,
757
+ context=context,
758
+ existing_content=content
759
+ )
760
+
761
+ # Save continuation generation for debugging
762
+ continuation_file = os.path.join(debug_dir, f"{safe_filename}_continuation_{continuation_count}.txt")
763
+ with open(continuation_file, 'w', encoding='utf-8') as f:
764
+ f.write(f"=== CONTINUATION {continuation_count} ===\n")
765
+ f.write(f"Tokens: {continuation_usage}\n")
766
+ f.write(f"Length: {len(continuation_content)} characters\n")
767
+ f.write(f"Truncation detected: {self._detect_truncation(continuation_content, target_file)}\n")
768
+ f.write(f"\n=== CONTENT ===\n")
769
+ f.write(continuation_content)
770
+
771
+ # Merge continuation with existing content
772
+ if continuation_content:
773
+ content += "\n\n" + continuation_content
774
+ # Update token usage
775
+ token_usage = {
776
+ "total_tokens": token_usage.get("total_tokens", 0) + continuation_usage.get("total_tokens", 0),
777
+ "prompt_tokens": token_usage.get("prompt_tokens", 0) + continuation_usage.get("prompt_tokens", 0),
778
+ "completion_tokens": token_usage.get("completion_tokens", 0) + continuation_usage.get("completion_tokens", 0),
779
+ }
780
+
781
+ # Save merged content for debugging
782
+ merged_file = os.path.join(debug_dir, f"{safe_filename}_merged_{continuation_count}.txt")
783
+ with open(merged_file, 'w', encoding='utf-8') as f:
784
+ f.write(f"=== MERGED CONTENT AFTER CONTINUATION {continuation_count} ===\n")
785
+ f.write(f"Total length: {len(content)} characters\n")
786
+ f.write(f"Truncation detected: {self._detect_truncation(content, target_file)}\n")
787
+ f.write(f"\n=== CONTENT ===\n")
788
+ f.write(content)
789
+ else:
790
+ # If continuation is empty, break to avoid infinite loop
791
+ break
792
+
793
+ # Clean up any markdown code fences that might have been added
794
+ content = self._clean_markdown_fences(content)
795
+
796
+ # Save final cleaned content for debugging
797
+ final_file = os.path.join(debug_dir, f"{safe_filename}_final.txt")
798
+ with open(final_file, 'w', encoding='utf-8') as f:
799
+ f.write(f"=== FINAL CLEANED CONTENT ===\n")
800
+ f.write(f"Total tokens: {token_usage}\n")
801
+ f.write(f"Final length: {len(content)} characters\n")
802
+ f.write(f"Continuations used: {continuation_count}\n")
803
+ f.write(f"\n=== CONTENT ===\n")
804
+ f.write(content)
805
+
806
+ return content, token_usage
807
+
808
+ def _clean_markdown_fences(self, content: str) -> str:
809
+ """
810
+ Remove markdown code fences that shouldn't be in the final content.
811
+ """
812
+ # Remove ```markdown at the beginning
813
+ if content.startswith('```markdown\n'):
814
+ content = content[12:] # Remove ```markdown\n
815
+
816
+ # Remove ``` at the end
817
+ if content.endswith('\n```'):
818
+ content = content[:-4] # Remove \n```
819
+ elif content.endswith('```'):
820
+ content = content[:-3] # Remove ```
821
+
822
+ # Remove any standalone ```markdown lines
823
+ lines = content.split('\n')
824
+ cleaned_lines = []
825
+ for line in lines:
826
+ if line.strip() == '```markdown':
827
+ continue
828
+ cleaned_lines.append(line)
829
+
830
+ return '\n'.join(cleaned_lines)
831
+
832
+ def _split_rmd_into_chunks(self, content: str) -> list[dict]:
833
+ chunks = []
834
+ if not content:
835
+ return chunks
836
+ lines = content.split('\n')
837
+ n = len(lines)
838
+ i = 0
839
+ if n >= 3 and lines[0].strip() == '---':
840
+ j = 1
841
+ while j < n and lines[j].strip() != '---':
842
+ j += 1
843
+ if j < n and lines[j].strip() == '---':
844
+ chunks.append({"type": "yaml", "content": '\n'.join(lines[0:j+1])})
845
+ i = j + 1
846
+ buffer = []
847
+ in_code = False
848
+ for k in range(i, n):
849
+ line = lines[k]
850
+ if line.strip().startswith('```'):
851
+ if in_code:
852
+ buffer.append(line)
853
+ chunks.append({"type": "code", "content": '\n'.join(buffer)})
854
+ buffer = []
855
+ in_code = False
856
+ else:
857
+ if buffer and any(s.strip() for s in buffer):
858
+ chunks.append({"type": "text", "content": '\n'.join(buffer)})
859
+ buffer = [line]
860
+ in_code = True
861
+ else:
862
+ buffer.append(line)
863
+ if buffer and any(s.strip() for s in buffer):
864
+ chunks.append({"type": "code" if in_code else "text", "content": '\n'.join(buffer)})
865
+ return chunks
866
+
867
+ def _generate_text_chunk(self, conv: CommonConversation, evaluation_report: dict, context: str, chunk_text: str) -> tuple[str, dict]:
868
+ LLM_CHUNK_PROMPT = (
869
+ "You are BioGuider improving a single markdown chunk of a larger RMarkdown document.\n\n"
870
+ "GOAL\nRefine ONLY the given chunk's prose per evaluation suggestions while preserving structure.\n"
871
+ "Do not add conclusions or new sections.\n\n"
872
+ "INPUTS\n- evaluation_report: <<{evaluation_report}>>\n- repo_context_excerpt: <<{context}>>\n- original_chunk:\n<<<\n{chunk}\n>>>\n\n"
873
+ "RULES\n- Preserve headers/formatting in this chunk.\n- Do not invent technical specs.\n- Output ONLY the refined chunk (no fences)."
874
+ )
875
+ system_prompt = LLM_CHUNK_PROMPT.format(
876
+ evaluation_report=json.dumps(evaluation_report)[:4000],
877
+ context=context[:1500],
878
+ chunk=chunk_text[:6000],
879
+ )
880
+ content, usage = conv.generate(system_prompt=system_prompt, instruction_prompt="Rewrite this chunk now.")
881
+ return content.strip(), usage
882
+
883
+ def _generate_full_document_chunked(self, target_file: str, evaluation_report: dict, context: str, original_content: str, debug_dir: str, safe_filename: str) -> tuple[str, dict]:
884
+ conv = CommonConversation(self.llm)
885
+ chunks = self._split_rmd_into_chunks(original_content)
886
+ merged = []
887
+ total_usage = {"total_tokens": 0, "prompt_tokens": 0, "completion_tokens": 0}
888
+ from datetime import datetime
889
+ for idx, ch in enumerate(chunks):
890
+ if ch["type"] in ("yaml", "code"):
891
+ merged.append(ch["content"])
892
+ continue
893
+ out, usage = self._generate_text_chunk(conv, evaluation_report, context, ch["content"])
894
+ if not out:
895
+ out = ch["content"]
896
+ merged.append(out)
897
+ try:
898
+ total_usage["total_tokens"] += int(usage.get("total_tokens", 0))
899
+ total_usage["prompt_tokens"] += int(usage.get("prompt_tokens", 0))
900
+ total_usage["completion_tokens"] += int(usage.get("completion_tokens", 0))
901
+ except Exception:
902
+ pass
903
+ chunk_file = os.path.join(debug_dir, f"{safe_filename}_chunk_{idx}.txt")
904
+ with open(chunk_file, 'w', encoding='utf-8') as f:
905
+ f.write(f"=== CHUNK {idx} ({ch['type']}) at {datetime.now().isoformat()} ===\n")
906
+ f.write(out)
907
+ content = '\n'.join(merged)
908
+ return content, total_usage
179
909
 
180
910