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.
- bioguider/agents/agent_utils.py +37 -13
- bioguider/generation/llm_content_generator.py +762 -32
- bioguider/generation/llm_injector.py +60 -7
- bioguider/generation/suggestion_extractor.py +26 -26
- bioguider/managers/generation_manager.py +168 -89
- {bioguider-0.2.32.dist-info → bioguider-0.2.34.dist-info}/METADATA +1 -1
- {bioguider-0.2.32.dist-info → bioguider-0.2.34.dist-info}/RECORD +9 -9
- {bioguider-0.2.32.dist-info → bioguider-0.2.34.dist-info}/LICENSE +0 -0
- {bioguider-0.2.32.dist-info → bioguider-0.2.34.dist-info}/WHEEL +0 -0
|
@@ -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
|
|
29
|
-
-
|
|
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
|
|
51
|
-
- System Requirements: Include
|
|
52
|
-
- Hardware Requirements: Include RAM/CPU recommendations
|
|
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
|
|
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
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
*
|
|
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
|
|
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
|
|
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
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
|
97
|
-
- ABSOLUTELY
|
|
98
|
-
- ABSOLUTELY
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
|