npcpy 1.0.26__py3-none-any.whl → 1.2.32__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.
Files changed (148) hide show
  1. npcpy/__init__.py +0 -7
  2. npcpy/data/audio.py +16 -99
  3. npcpy/data/image.py +43 -42
  4. npcpy/data/load.py +83 -124
  5. npcpy/data/text.py +28 -28
  6. npcpy/data/video.py +8 -32
  7. npcpy/data/web.py +51 -23
  8. npcpy/ft/diff.py +110 -0
  9. npcpy/ft/ge.py +115 -0
  10. npcpy/ft/memory_trainer.py +171 -0
  11. npcpy/ft/model_ensembler.py +357 -0
  12. npcpy/ft/rl.py +360 -0
  13. npcpy/ft/sft.py +248 -0
  14. npcpy/ft/usft.py +128 -0
  15. npcpy/gen/audio_gen.py +24 -0
  16. npcpy/gen/embeddings.py +13 -13
  17. npcpy/gen/image_gen.py +262 -117
  18. npcpy/gen/response.py +615 -415
  19. npcpy/gen/video_gen.py +53 -7
  20. npcpy/llm_funcs.py +1869 -437
  21. npcpy/main.py +1 -1
  22. npcpy/memory/command_history.py +844 -510
  23. npcpy/memory/kg_vis.py +833 -0
  24. npcpy/memory/knowledge_graph.py +892 -1845
  25. npcpy/memory/memory_processor.py +81 -0
  26. npcpy/memory/search.py +188 -90
  27. npcpy/mix/debate.py +192 -3
  28. npcpy/npc_compiler.py +1672 -801
  29. npcpy/npc_sysenv.py +593 -1266
  30. npcpy/serve.py +3120 -0
  31. npcpy/sql/ai_function_tools.py +257 -0
  32. npcpy/sql/database_ai_adapters.py +186 -0
  33. npcpy/sql/database_ai_functions.py +163 -0
  34. npcpy/sql/model_runner.py +19 -19
  35. npcpy/sql/npcsql.py +706 -507
  36. npcpy/sql/sql_model_compiler.py +156 -0
  37. npcpy/tools.py +183 -0
  38. npcpy/work/plan.py +13 -279
  39. npcpy/work/trigger.py +3 -3
  40. npcpy-1.2.32.dist-info/METADATA +803 -0
  41. npcpy-1.2.32.dist-info/RECORD +54 -0
  42. npcpy/data/dataframes.py +0 -171
  43. npcpy/memory/deep_research.py +0 -125
  44. npcpy/memory/sleep.py +0 -557
  45. npcpy/modes/_state.py +0 -78
  46. npcpy/modes/alicanto.py +0 -1075
  47. npcpy/modes/guac.py +0 -785
  48. npcpy/modes/mcp_npcsh.py +0 -822
  49. npcpy/modes/npc.py +0 -213
  50. npcpy/modes/npcsh.py +0 -1158
  51. npcpy/modes/plonk.py +0 -409
  52. npcpy/modes/pti.py +0 -234
  53. npcpy/modes/serve.py +0 -1637
  54. npcpy/modes/spool.py +0 -312
  55. npcpy/modes/wander.py +0 -549
  56. npcpy/modes/yap.py +0 -572
  57. npcpy/npc_team/alicanto.npc +0 -2
  58. npcpy/npc_team/alicanto.png +0 -0
  59. npcpy/npc_team/assembly_lines/test_pipeline.py +0 -181
  60. npcpy/npc_team/corca.npc +0 -13
  61. npcpy/npc_team/foreman.npc +0 -7
  62. npcpy/npc_team/frederic.npc +0 -6
  63. npcpy/npc_team/frederic4.png +0 -0
  64. npcpy/npc_team/guac.png +0 -0
  65. npcpy/npc_team/jinxs/automator.jinx +0 -18
  66. npcpy/npc_team/jinxs/bash_executer.jinx +0 -31
  67. npcpy/npc_team/jinxs/calculator.jinx +0 -11
  68. npcpy/npc_team/jinxs/edit_file.jinx +0 -96
  69. npcpy/npc_team/jinxs/file_chat.jinx +0 -14
  70. npcpy/npc_team/jinxs/gui_controller.jinx +0 -28
  71. npcpy/npc_team/jinxs/image_generation.jinx +0 -29
  72. npcpy/npc_team/jinxs/internet_search.jinx +0 -30
  73. npcpy/npc_team/jinxs/local_search.jinx +0 -152
  74. npcpy/npc_team/jinxs/npcsh_executor.jinx +0 -31
  75. npcpy/npc_team/jinxs/python_executor.jinx +0 -8
  76. npcpy/npc_team/jinxs/screen_cap.jinx +0 -25
  77. npcpy/npc_team/jinxs/sql_executor.jinx +0 -33
  78. npcpy/npc_team/kadiefa.npc +0 -3
  79. npcpy/npc_team/kadiefa.png +0 -0
  80. npcpy/npc_team/npcsh.ctx +0 -9
  81. npcpy/npc_team/npcsh_sibiji.png +0 -0
  82. npcpy/npc_team/plonk.npc +0 -2
  83. npcpy/npc_team/plonk.png +0 -0
  84. npcpy/npc_team/plonkjr.npc +0 -2
  85. npcpy/npc_team/plonkjr.png +0 -0
  86. npcpy/npc_team/sibiji.npc +0 -5
  87. npcpy/npc_team/sibiji.png +0 -0
  88. npcpy/npc_team/spool.png +0 -0
  89. npcpy/npc_team/templates/analytics/celona.npc +0 -0
  90. npcpy/npc_team/templates/hr_support/raone.npc +0 -0
  91. npcpy/npc_team/templates/humanities/eriane.npc +0 -4
  92. npcpy/npc_team/templates/it_support/lineru.npc +0 -0
  93. npcpy/npc_team/templates/marketing/slean.npc +0 -4
  94. npcpy/npc_team/templates/philosophy/maurawa.npc +0 -0
  95. npcpy/npc_team/templates/sales/turnic.npc +0 -4
  96. npcpy/npc_team/templates/software/welxor.npc +0 -0
  97. npcpy/npc_team/yap.png +0 -0
  98. npcpy/routes.py +0 -958
  99. npcpy/work/mcp_helpers.py +0 -357
  100. npcpy/work/mcp_server.py +0 -194
  101. npcpy-1.0.26.data/data/npcpy/npc_team/alicanto.npc +0 -2
  102. npcpy-1.0.26.data/data/npcpy/npc_team/alicanto.png +0 -0
  103. npcpy-1.0.26.data/data/npcpy/npc_team/automator.jinx +0 -18
  104. npcpy-1.0.26.data/data/npcpy/npc_team/bash_executer.jinx +0 -31
  105. npcpy-1.0.26.data/data/npcpy/npc_team/calculator.jinx +0 -11
  106. npcpy-1.0.26.data/data/npcpy/npc_team/celona.npc +0 -0
  107. npcpy-1.0.26.data/data/npcpy/npc_team/corca.npc +0 -13
  108. npcpy-1.0.26.data/data/npcpy/npc_team/edit_file.jinx +0 -96
  109. npcpy-1.0.26.data/data/npcpy/npc_team/eriane.npc +0 -4
  110. npcpy-1.0.26.data/data/npcpy/npc_team/file_chat.jinx +0 -14
  111. npcpy-1.0.26.data/data/npcpy/npc_team/foreman.npc +0 -7
  112. npcpy-1.0.26.data/data/npcpy/npc_team/frederic.npc +0 -6
  113. npcpy-1.0.26.data/data/npcpy/npc_team/frederic4.png +0 -0
  114. npcpy-1.0.26.data/data/npcpy/npc_team/guac.png +0 -0
  115. npcpy-1.0.26.data/data/npcpy/npc_team/gui_controller.jinx +0 -28
  116. npcpy-1.0.26.data/data/npcpy/npc_team/image_generation.jinx +0 -29
  117. npcpy-1.0.26.data/data/npcpy/npc_team/internet_search.jinx +0 -30
  118. npcpy-1.0.26.data/data/npcpy/npc_team/kadiefa.npc +0 -3
  119. npcpy-1.0.26.data/data/npcpy/npc_team/kadiefa.png +0 -0
  120. npcpy-1.0.26.data/data/npcpy/npc_team/lineru.npc +0 -0
  121. npcpy-1.0.26.data/data/npcpy/npc_team/local_search.jinx +0 -152
  122. npcpy-1.0.26.data/data/npcpy/npc_team/maurawa.npc +0 -0
  123. npcpy-1.0.26.data/data/npcpy/npc_team/npcsh.ctx +0 -9
  124. npcpy-1.0.26.data/data/npcpy/npc_team/npcsh_executor.jinx +0 -31
  125. npcpy-1.0.26.data/data/npcpy/npc_team/npcsh_sibiji.png +0 -0
  126. npcpy-1.0.26.data/data/npcpy/npc_team/plonk.npc +0 -2
  127. npcpy-1.0.26.data/data/npcpy/npc_team/plonk.png +0 -0
  128. npcpy-1.0.26.data/data/npcpy/npc_team/plonkjr.npc +0 -2
  129. npcpy-1.0.26.data/data/npcpy/npc_team/plonkjr.png +0 -0
  130. npcpy-1.0.26.data/data/npcpy/npc_team/python_executor.jinx +0 -8
  131. npcpy-1.0.26.data/data/npcpy/npc_team/raone.npc +0 -0
  132. npcpy-1.0.26.data/data/npcpy/npc_team/screen_cap.jinx +0 -25
  133. npcpy-1.0.26.data/data/npcpy/npc_team/sibiji.npc +0 -5
  134. npcpy-1.0.26.data/data/npcpy/npc_team/sibiji.png +0 -0
  135. npcpy-1.0.26.data/data/npcpy/npc_team/slean.npc +0 -4
  136. npcpy-1.0.26.data/data/npcpy/npc_team/spool.png +0 -0
  137. npcpy-1.0.26.data/data/npcpy/npc_team/sql_executor.jinx +0 -33
  138. npcpy-1.0.26.data/data/npcpy/npc_team/test_pipeline.py +0 -181
  139. npcpy-1.0.26.data/data/npcpy/npc_team/turnic.npc +0 -4
  140. npcpy-1.0.26.data/data/npcpy/npc_team/welxor.npc +0 -0
  141. npcpy-1.0.26.data/data/npcpy/npc_team/yap.png +0 -0
  142. npcpy-1.0.26.dist-info/METADATA +0 -827
  143. npcpy-1.0.26.dist-info/RECORD +0 -139
  144. npcpy-1.0.26.dist-info/entry_points.txt +0 -11
  145. /npcpy/{modes → ft}/__init__.py +0 -0
  146. {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/WHEEL +0 -0
  147. {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/licenses/LICENSE +0 -0
  148. {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/top_level.txt +0 -0
npcpy/modes/alicanto.py DELETED
@@ -1,1075 +0,0 @@
1
- import os
2
- import random
3
- from typing import List, Dict, Any, Optional, Union, Tuple
4
- import numpy as np
5
- from collections import defaultdict, Counter
6
- import itertools
7
- import matplotlib.pyplot as plt
8
- from matplotlib.figure import Figure
9
- from io import BytesIO
10
- import base64
11
- import datetime
12
- import tempfile
13
- import subprocess
14
- import networkx as nx
15
-
16
- from npcpy.npc_compiler import NPC
17
- from npcpy.llm_funcs import get_llm_response
18
- from npcpy.npc_sysenv import NPCSH_CHAT_MODEL, NPCSH_CHAT_PROVIDER, print_and_process_stream_with_markdown
19
- from npcpy.memory.deep_research import consolidate_research
20
- from npcpy.memory.knowledge_graph import extract_facts, identify_groups, assign_groups_to_fact
21
-
22
- def generate_random_npcs(num_npcs: int, model: str, provider: str, request: str) -> List[NPC]:
23
- """
24
- Generate a diverse set of NPCs with different expertise and perspectives
25
- related to the research request.
26
- """
27
- # For single NPC, use a simpler approach to avoid unnecessary LLM calls
28
- if num_npcs == 1:
29
- # Generate directly without complex JSON parsing
30
- name = f"Expert Researcher on {request}"
31
- expertise = "Interdisciplinary semantic theory researcher"
32
- background = "Extensive experience in linguistics, cognitive science, and NLP"
33
- perspective = "Combines formal logic with empirical linguistic evidence"
34
- quirk = "Uses mathematical metaphors to explain language phenomena"
35
- biases = "May favor formal approaches over descriptive linguistics"
36
-
37
- system_prompt = f"""
38
- You are {name}, {expertise}.
39
-
40
- Background: {background}
41
-
42
- Your perspective: {perspective}
43
-
44
- Your methodological quirk: {quirk}
45
-
46
- Note: Be aware that you may have these biases: {biases}
47
-
48
- Your task is to research the given topic thoroughly, focusing on your unique perspective.
49
- Challenge conventional thinking and identify unexpected connections.
50
- Your insights should be provocative and novel.
51
-
52
- IMPORTANT: You must be extremely concise. Limit responses to 50-75 words maximum.
53
- """
54
-
55
- npc = NPC(name=name, primary_directive=f"Research expert on {request}")
56
- npc.system_prompt = system_prompt
57
- return [npc]
58
-
59
- # Generate diverse expert personas based on the research topic
60
- prompt = f"""
61
- For the research topic: "{request}"
62
-
63
- Generate {num_npcs} diverse expert personas who would have different valuable perspectives on this topic.
64
- I need truly diverse and unusual viewpoints that can lead to innovative insights.
65
-
66
- For each expert, provide:
67
- 1. A name
68
- 2. Their field of expertise (be creative - include unconventional and interdisciplinary fields)
69
- 3. Their background/experience (include unusual career paths and perspectives)
70
- 4. Their unique perspective or approach to the topic (emphasize contrarian, minority, or unexpected viewpoints)
71
- 5. A methodological quirk that makes their research approach unusual
72
- 6. Any potential biases they might have
73
- """
74
-
75
- response = get_llm_response(
76
- prompt=prompt,
77
- model=model,
78
- provider=provider,
79
- format="json" # Directly request JSON format
80
- )
81
-
82
- # Response will be properly structured JSON from get_llm_response
83
- experts_data = response.get('response', [])
84
-
85
- # Create NPC instances from expert data
86
- npcs = []
87
-
88
- # Handle experts_data safely whether it's a list or not
89
- if isinstance(experts_data, list):
90
- experts_to_process = experts_data[:num_npcs]
91
- else:
92
- # If not a list, try to convert or use as a single item
93
- if isinstance(experts_data, dict):
94
- experts_to_process = [experts_data]
95
- else:
96
- # Create a basic expert as fallback
97
- experts_to_process = [{
98
- "name": f"Expert_1",
99
- "expertise": "Interdisciplinary researcher",
100
- "background": "Diverse academic and practical experience",
101
- "perspective": "Balanced analysis with focus on innovative connections",
102
- "methodological_quirk": "Uses unconventional conceptual frameworks",
103
- "biases": "Tends toward theoretical rather than practical solutions"
104
- }]
105
-
106
- for expert in experts_to_process:
107
- name = expert.get("name", f"Expert_{len(npcs)}")
108
-
109
- # Create a system prompt that defines this NPC's expertise and perspective
110
- system_prompt = f"""
111
- You are {name}, {expert.get('expertise', 'an expert researcher')}.
112
-
113
- Background: {expert.get('background', 'You have extensive knowledge in your field.')}
114
-
115
- Your perspective: {expert.get('perspective', 'You provide detailed, balanced analysis.')}
116
-
117
- Your methodological quirk: {expert.get('methodological_quirk', 'You approach problems in unconventional ways.')}
118
-
119
- Note: Be aware that you may have these biases: {expert.get('biases', 'None specifically noted.')}
120
-
121
- Your task is to research the given topic thoroughly, focusing on your unique perspective and methodological approach.
122
- Challenge conventional thinking, explore neglected angles, and identify unexpected connections or contradictions.
123
- Your insights should be provocative and novel, not just rehashing mainstream views.
124
-
125
- IMPORTANT: You must be extremely concise. Limit responses to 50-75 words maximum. Focus on substance over verbosity.
126
- Prioritize precision, clarity, and insight density. Eliminate unnecessary words and focus on communicating
127
- the essence of your insights in the most efficient way possible.
128
- """
129
-
130
- # Create NPC with name and primary_directive (required parameters)
131
- npc = NPC(name=name, primary_directive=f"Research expert on {request}")
132
- npc.system_prompt = system_prompt
133
- npcs.append(npc)
134
-
135
- return npcs
136
-
137
- def generate_research_chain(request: str, npc: NPC, depth: int, memory: int = 3,
138
- context: str = None, model: str = None, provider: str = None,
139
- exploration_factor: float = 0.3,
140
- creativity_factor: float = 0.5) -> List[str]:
141
- """
142
- Generate a chain of research thoughts from a single NPC, diving deeper with each step.
143
-
144
- Args:
145
- request: The research question/topic
146
- npc: The NPC generating the research
147
- depth: How many steps of research to perform
148
- memory: How many previous steps to include in context
149
- context: Additional context to include
150
- model: LLM model to use
151
- provider: LLM provider to use
152
- exploration_factor: Probability (0-1) of exploring a tangential direction
153
- creativity_factor: Probability (0-1) of pursuing highly creative or unusual ideas
154
-
155
- Returns:
156
- List of research findings/thoughts from this chain
157
- """
158
- chain = []
159
-
160
- # Initial research prompt
161
- initial_prompt = f"""
162
- Research request: {request}
163
-
164
- {f"Additional context: {context}" if context else ""}
165
-
166
- As {npc.name}, begin your research process by:
167
- 1. Analyzing what you know about this topic
168
- 2. Identifying key questions that need to be explored
169
- 3. Providing initial insights based on your expertise and unique perspective
170
-
171
- BE EXTREMELY CONCISE. Focus on substance over wordiness. Provide clear, high-value insights in 50-75 words maximum.
172
- """
173
-
174
- response = get_llm_response(prompt=initial_prompt, model=model, provider=provider, npc=npc, temperature=0.7)
175
- initial_findings = response.get('response', '')
176
- if isinstance(initial_findings, (list, dict)) or hasattr(initial_findings, '__iter__') and not isinstance(initial_findings, (str, bytes)):
177
- initial_findings = ''.join([str(chunk) for chunk in initial_findings])
178
-
179
- chain.append(initial_findings)
180
-
181
- # For each level of depth, continue the research
182
- for i in range(1, depth):
183
- # Get recent memory to include as context
184
- memory_context = "\n\n".join(chain[-memory:]) if len(chain) > 0 else ""
185
-
186
- # Simple follow-up prompt without specific research modes
187
- next_prompt = f"""
188
- Research request: {request}
189
-
190
- Recent research findings:
191
- {memory_context}
192
-
193
- As {npc.name}, continue your research on this topic. Build on previous insights and explore new aspects.
194
-
195
- BE EXTREMELY CONCISE. Keep your response to 50-75 words maximum.
196
- """
197
-
198
- response = get_llm_response(prompt=next_prompt, model=model, provider=provider, npc=npc, temperature=0.7)
199
- next_findings = response.get('response', '')
200
- if isinstance(next_findings, (list, dict)) or hasattr(next_findings, '__iter__') and not isinstance(next_findings, (str, bytes)):
201
- next_findings = ''.join([str(chunk) for chunk in next_findings])
202
-
203
- chain.append(next_findings)
204
-
205
- return chain
206
-
207
- def format_facts_list(facts: List[str]) -> str:
208
- """Format a list of facts for display in a report"""
209
- return "\n".join([f"• {fact}" for fact in facts])
210
-
211
- def simulate_experiments(research: Dict[str, Any], request: str, model: str = None, provider: str = None, max_experiments: int = None) -> Dict[str, Dict[str, Any]]:
212
- """
213
- Simulate thought experiments based on research findings
214
-
215
- Args:
216
- research: Consolidated research data
217
- request: Original research question
218
- model: LLM model to use
219
- provider: LLM provider to use
220
- max_experiments: Maximum number of experiments to generate
221
-
222
- Returns:
223
- Dictionary mapping experiment titles to experiment data
224
- """
225
- # Prepare context with key facts
226
- facts_context = ""
227
-
228
- # Add facts from thematic groups
229
- if "fact_groups" in research:
230
- for group, facts in list(research["fact_groups"].items())[:5]: # Use top 5 groups
231
- facts_context += f"\n\nThematic Group: {group}\n"
232
- facts_context += format_facts_list(facts)
233
-
234
- # Add insights from combinations
235
- if "combination_insights" in research:
236
- facts_context += "\n\nEmergent Insights:\n"
237
- for combo in research["combination_insights"][:3]: # Use top 3 insights
238
- facts_context += f"• {combo.get('emergent_insight', '')}\n"
239
-
240
- # Create prompt to design experiments
241
- prompt = f"""
242
- You are a creative research scientist exploring the topic: "{request}"
243
-
244
- Based on the following research findings:
245
-
246
- {facts_context}
247
-
248
- Design {max_experiments if max_experiments else "3-5"} thought experiments that could test, validate, or extend these insights.
249
-
250
- For each experiment:
251
- 1. Create a descriptive title that captures the experiment's focus
252
- 2. Describe the experimental design/methodology (be specific and detailed)
253
- 3. Predict the potential results and their implications
254
- 4. Explain how these results would advance our understanding of {request}
255
-
256
- Format your response as JSON with this structure:
257
- {{
258
- "experiment_title_1": {{
259
- "design": "detailed description of experimental design",
260
- "results": "predicted results and implications"
261
- }},
262
- "experiment_title_2": {{
263
- ...
264
- }}
265
- }}
266
-
267
- Be bold and imaginative in your experimental designs. Consider unconventional approaches,
268
- simulations, thought experiments, and interdisciplinary methods.
269
- """
270
-
271
- response = get_llm_response(prompt=prompt, model=model, provider=provider, temperature=0.8, format="json")
272
- experiments = response.get("response", {})
273
-
274
- # Limit experiments if needed
275
- if max_experiments and isinstance(experiments, dict) and len(experiments) > max_experiments:
276
- # Sort by title length (approximating complexity/interestingness)
277
- sorted_exps = sorted(experiments.items(), key=lambda x: len(x[0]), reverse=True)
278
- experiments = dict(sorted_exps[:max_experiments])
279
-
280
- return experiments
281
-
282
- def alicanto(request: str,
283
- num_npcs: int = 5,
284
- depth: int = 3, memory: int = 3,
285
- context: str = None,
286
- model: str = None,
287
- provider: str = None,
288
- exploration_factor: float = 0.3,
289
- creativity_factor: float = 0.5,
290
- output_format: str = "report",
291
- max_facts_per_chain: int = None,
292
- max_thematic_groups: int = None,
293
- max_criticisms_per_group: int = None,
294
- max_conceptual_combinations: int = None,
295
- max_experiments: int = None,
296
- generate_pdf: bool = True) -> Dict[str, Any]:
297
- """
298
- Alicanto: Generate diverse research insights by coordinating multiple NPCs with different expertise.
299
-
300
- Args:
301
- request: The research question/topic
302
- num_npcs: Number of NPCs to generate (with different expertise)
303
- depth: Depth of research for each NPC
304
- memory: How many previous steps to include in context
305
- context: Additional context to include
306
- model: LLM model to use
307
- provider: LLM provider to use
308
- exploration_factor: Probability (0-1) of exploring a tangential direction
309
- creativity_factor: Probability (0-1) of pursuing highly creative or unusual ideas
310
- output_format: Format of the output ("report", "json", "markdown")
311
- max_facts_per_chain: Maximum number of facts to extract per research chain
312
- max_thematic_groups: Maximum number of thematic groups to identify
313
- max_criticisms_per_group: Maximum number of criticisms per thematic group
314
- max_conceptual_combinations: Maximum number of conceptual combinations to generate
315
- max_experiments: Maximum number of experiments to generate
316
- generate_pdf: Whether to generate a PDF report
317
-
318
- Returns:
319
- Dictionary with research results
320
- """
321
- # Use default model/provider if not specified
322
- if model is None:
323
- model = NPCSH_CHAT_MODEL
324
- if provider is None:
325
- provider = NPCSH_CHAT_PROVIDER
326
-
327
- # Generate researcher NPCs with diverse expertise
328
- print(f"Generating {num_npcs} diverse researcher NPCs...")
329
- researchers = generate_random_npcs(num_npcs, model, provider, request)
330
-
331
- # Generate research chains for each NPC
332
- print(f"Generating research chains (depth={depth})...")
333
- research_chains = {}
334
- facts_by_researcher = {}
335
-
336
- for npc in researchers:
337
- print(f" Research chain from {npc.name}...")
338
- chain = generate_research_chain(
339
- request=request,
340
- npc=npc,
341
- depth=depth,
342
- memory=memory,
343
- context=context,
344
- model=model,
345
- provider=provider,
346
- exploration_factor=exploration_factor,
347
- creativity_factor=creativity_factor
348
- )
349
- research_chains[npc.name] = chain
350
-
351
- # Extract facts from chain
352
- print(f" Extracting facts from {npc.name}'s research...")
353
- facts = extract_facts("\n\n".join(chain), model=model, provider=provider, npc=npc, context=request)
354
-
355
- # Limit facts if specified
356
- if max_facts_per_chain is not None and len(facts) > max_facts_per_chain:
357
- facts = facts[:max_facts_per_chain]
358
-
359
- facts_by_researcher[npc.name] = facts
360
- print({"fact_list": facts})
361
-
362
- # Identify thematic groups across all research
363
- print("Identifying thematic groups across all research insights...")
364
- all_facts = []
365
- for researcher_facts in facts_by_researcher.values():
366
- all_facts.extend(researcher_facts)
367
-
368
- groups = identify_groups(all_facts, model=model, provider=provider)
369
-
370
- # Limit number of groups if specified
371
- if max_thematic_groups is not None and len(groups) > max_thematic_groups:
372
- groups = groups[:max_thematic_groups]
373
-
374
- # Assign facts to groups
375
- fact_groups = {group: [] for group in groups}
376
- for fact in all_facts:
377
- group_assignments = assign_groups_to_fact(fact, groups, model=model, provider=provider)
378
- assigned_groups = group_assignments.get("groups", [])
379
- for group in assigned_groups:
380
- if group in fact_groups:
381
- fact_groups[group].append(fact)
382
-
383
- # Evaluate thematic groups
384
- print("Evaluating thematic groups for quality and risk...")
385
- group_evaluations = evaluate_thematic_groups(
386
- fact_groups,
387
- request,
388
- model=model,
389
- provider=provider,
390
- max_criticisms=max_criticisms_per_group
391
- )
392
-
393
- # Generate group summaries
394
- group_summaries = {}
395
- for group_name, facts in fact_groups.items():
396
- if not facts:
397
- continue
398
-
399
- prompt = f"""
400
- Summarize the key insights from this thematic group of research findings on the topic:
401
- "{request}"
402
-
403
- Thematic Group: {group_name}
404
-
405
- Findings:
406
- {format_facts_list(facts)}
407
-
408
- Provide a concise, coherent synthesis that captures the core ideas,
409
- emphasizes what's most novel or significant, and suggests potential implications.
410
- Keep your response to 200-300 words.
411
- """
412
-
413
- response = get_llm_response(prompt=prompt, model=model, provider=provider)
414
- summary = response.get('response', '')
415
- if isinstance(summary, (list, dict)) or hasattr(summary, '__iter__') and not isinstance(summary, (str, bytes)):
416
- summary = ''.join([str(chunk) for chunk in summary])
417
-
418
- group_summaries[group_name] = summary
419
-
420
- # Generate conceptual combinations to spark novel ideas
421
- print("Generating conceptual combinations to spark novel insights...")
422
- fact_lists = list(facts_by_researcher.values())
423
- combinations = generate_conceptual_combinations(
424
- fact_lists,
425
- sample_size=min(3, len(all_facts)),
426
- num_combinations=max_conceptual_combinations if max_conceptual_combinations is not None else 5
427
- )
428
-
429
- # Analyze combinations for emergent insights
430
- print("Analyzing conceptual combinations for emergent insights...")
431
- combination_insights = analyze_conceptual_combinations(
432
- combinations,
433
- request,
434
- model=model,
435
- provider=provider
436
- )
437
-
438
- # Identify meta-patterns
439
- print("Identifying meta-patterns across research approaches...")
440
- meta_patterns = identify_patterns_across_chains(research_chains, model=model, provider=provider)
441
-
442
- # Generate consolidated research summary
443
- print("Consolidating research into comprehensive synthesis...")
444
-
445
- # Extract key points for integration
446
- integration_points = []
447
-
448
- # Add top facts from each thematic group
449
- for group, facts in fact_groups.items():
450
- if facts:
451
- integration_points.append(f"From thematic group '{group}':")
452
- for fact in facts[:3]: # Top 3 facts per group
453
- integration_points.append(f"- {fact}")
454
-
455
- # Add insights from combinations
456
- for insight in combination_insights[:3]: # Top 3 insights
457
- integration_points.append(f"Emergent insight: {insight.get('emergent_insight', '')}")
458
-
459
- # Add key points from meta-analysis
460
- integration_points.append(f"Meta-analysis insight: {meta_patterns.get('meta_analysis', '')[:300]}...")
461
-
462
- # Generate integration
463
- integration_prompt = f"""
464
- Consolidate these diverse research findings into a comprehensive, integrative analysis of the topic:
465
- "{request}"
466
-
467
- Key points from the research:
468
- {format_facts_list(integration_points)}
469
-
470
- Your consolidation should:
471
- 1. Provide a coherent synthesis of the diverse perspectives
472
- 2. Identify the most significant findings and patterns
473
- 3. Note any tensions, contradictions, or complementary insights
474
- 4. Suggest an integrated framework for understanding the topic
475
- 5. Briefly outline implications and future directions
476
-
477
- Aim for a comprehensive, balanced, and insightful analysis (300-500 words).
478
- """
479
-
480
- integration_response = get_llm_response(integration_prompt, model=model, provider=provider)
481
- integration = integration_response.get('response', '')
482
- if isinstance(integration, (list, dict)) or hasattr(integration, '__iter__') and not isinstance(integration, (str, bytes)):
483
- integration = ''.join([str(chunk) for chunk in integration])
484
-
485
- # Create concise summary
486
- summary_prompt = f"""
487
- Create a concise executive summary (150 words max) of this research on:
488
- "{request}"
489
-
490
- Integration:
491
- {integration}
492
-
493
- Focus on the most significant findings and implications. This should be suitable for someone who only has time to read a brief overview.
494
- """
495
-
496
- summary_response = get_llm_response(summary_prompt, model=model, provider=provider)
497
- ideas_summarized = summary_response.get('response', '')
498
- if isinstance(ideas_summarized, (list, dict)) or hasattr(ideas_summarized, '__iter__') and not isinstance(ideas_summarized, (str, bytes)):
499
- ideas_summarized = ''.join([str(chunk) for chunk in ideas_summarized])
500
-
501
- # Simulate experiments
502
- print("Generating simulated experiments...")
503
- research_results = {
504
- "research_request": request,
505
- "research_chains": research_chains,
506
- "fact_groups": fact_groups,
507
- "group_evaluations": group_evaluations,
508
- "group_summaries": group_summaries,
509
- "combination_insights": combination_insights,
510
- "meta_patterns": meta_patterns,
511
- "integration": integration,
512
- "ideas_summarized": ideas_summarized
513
- }
514
-
515
- experiments = simulate_experiments(
516
- research_results,
517
- request,
518
- model=model,
519
- provider=provider,
520
- max_experiments=max_experiments
521
- )
522
-
523
- # Generate PDF report if requested
524
- pdf_path = None
525
- if generate_pdf:
526
- pdf_path = generate_pdf_report(request, model, provider, research_results, experiments)
527
-
528
- # Final research results
529
- research_results["experiments"] = experiments
530
- research_results["pdf_path"] = pdf_path
531
-
532
- return research_results
533
-
534
- def evaluate_thematic_groups(fact_groups: Dict[str, List[str]], request: str, model: str = None, provider: str = None, max_criticisms: int = None) -> Dict[str, Dict[str, int]]:
535
- """
536
- Evaluate each thematic group for quality, potential risks, and biases.
537
-
538
- Args:
539
- fact_groups: Dictionary mapping group names to lists of facts
540
- request: The original research question
541
- model: LLM model to use
542
- provider: LLM provider to use
543
- max_criticisms: Maximum number of criticisms to generate per group
544
-
545
- Returns:
546
- Dictionary mapping group names to evaluation metrics
547
- """
548
- evaluations = {}
549
-
550
- for group_name, facts in fact_groups.items():
551
- facts_text = format_facts_list(facts)
552
-
553
- prompt = f"""
554
- Evaluate this thematic group of research insights on the topic:
555
- "{request}"
556
-
557
- Thematic Group: {group_name}
558
-
559
- Insights:
560
- {facts_text}
561
-
562
- Evaluate this group of insights on a scale of 1-10 (where 10 is highest) for:
563
- 1. Novelty: How original and non-obvious are these insights?
564
- 2. Depth: How deeply do they explore the underlying concepts?
565
- 3. Practicality: How useful are these insights for further research or application?
566
- 4. Evidence: How well-supported do these claims appear to be?
567
- 5. Risk: What is the chance that these insights lead to problematic directions or dead ends?
568
-
569
- Then identify potential weaknesses, biases, or limitations in these insights.
570
- {f"Provide exactly {max_criticisms} criticisms." if max_criticisms is not None else ""}
571
-
572
- Format your response as:
573
- Novelty: [score]
574
- Depth: [score]
575
- Practicality: [score]
576
- Evidence: [score]
577
- Risk: [score]
578
-
579
- Criticisms:
580
- 1. [First criticism]
581
- 2. [Second criticism]
582
- ...
583
- """
584
-
585
- response = get_llm_response(prompt=prompt, model=model, provider=provider)
586
- eval_text = response.get('response', '')
587
- if isinstance(eval_text, (list, dict)) or hasattr(eval_text, '__iter__') and not isinstance(eval_text, (str, bytes)):
588
- eval_text = ''.join([str(chunk) for chunk in eval_text])
589
-
590
- # Parse scores
591
- scores = {}
592
- criticisms = []
593
- in_criticisms = False
594
-
595
- for line in eval_text.split('\n'):
596
- line = line.strip()
597
- if not line:
598
- continue
599
-
600
- if line.lower() == "criticisms:":
601
- in_criticisms = True
602
- continue
603
-
604
- if in_criticisms:
605
- # Parse criticisms
606
- if line[0].isdigit() and line[1:].startswith('. '):
607
- criticism = line[line.find(' ')+1:].strip()
608
- criticisms.append(criticism)
609
- else:
610
- # Parse scores
611
- if ':' in line:
612
- metric, score_str = line.split(':', 1)
613
- metric = metric.strip()
614
- try:
615
- score = int(score_str.strip())
616
- scores[metric] = score
617
- except ValueError:
618
- pass
619
-
620
- # Apply criticism limit if specified
621
- if max_criticisms is not None and len(criticisms) > max_criticisms:
622
- criticisms = criticisms[:max_criticisms]
623
-
624
- evaluations[group_name] = {
625
- **scores,
626
- "criticisms": criticisms
627
- }
628
-
629
- return evaluations
630
-
631
- def generate_conceptual_combinations(fact_lists: List[List[str]], sample_size: int = 3, num_combinations: int = 5) -> List[Dict]:
632
- """
633
- Generate interesting combinations of facts from different researchers to spark novel ideas.
634
-
635
- Args:
636
- fact_lists: List of fact lists from different NPCs
637
- sample_size: Number of facts to include in each combination
638
- num_combinations: Number of combinations to generate
639
-
640
- Returns:
641
- List of dictionaries containing the combinations and generated insights
642
- """
643
- # Flatten facts with researcher ID
644
- all_facts_with_source = []
645
- for i, facts in enumerate(fact_lists):
646
- for fact in facts:
647
- all_facts_with_source.append((i, fact))
648
-
649
- # Generate random combinations
650
- combinations = []
651
- for _ in range(num_combinations):
652
- if len(all_facts_with_source) <= sample_size:
653
- sample = all_facts_with_source
654
- else:
655
- sample = random.sample(all_facts_with_source, sample_size)
656
-
657
- combinations.append({
658
- "facts": [fact for _, fact in sample],
659
- "sources": [source for source, _ in sample]
660
- })
661
-
662
- return combinations
663
-
664
- def analyze_conceptual_combinations(combinations: List[Dict], request: str, model: str = None, provider: str = None) -> List[Dict]:
665
- """
666
- Analyze combinations of facts to identify emergent patterns and generate novel hypotheses.
667
-
668
- Args:
669
- combinations: List of fact combinations
670
- request: The original research question
671
- model: LLM model to use
672
- provider: LLM provider to use
673
-
674
- Returns:
675
- List of dictionaries with analysis results
676
- """
677
- results = []
678
-
679
- for i, combo in enumerate(combinations):
680
- facts_formatted = format_facts_list(combo["facts"])
681
-
682
- prompt = f"""
683
- Consider these seemingly unrelated insights from different researchers exploring the topic:
684
- "{request}"
685
-
686
- {facts_formatted}
687
-
688
- Your task is to identify a non-obvious connection, pattern, or insight that emerges when these ideas are juxtaposed.
689
- Focus on discovering something truly novel that none of the individual researchers may have recognized.
690
-
691
- 1. Identify a surprising emergent pattern or connection
692
- 2. Develop a novel hypothesis or research question based on this pattern
693
- 3. Explain how this insight challenges or extends conventional thinking on the topic
694
- 4. Suggest an unconventional methodology or approach to explore this new direction
695
-
696
- Be bold, imaginative, and interdisciplinary in your thinking.
697
- """
698
-
699
- response = get_llm_response(prompt=prompt, model=model, provider=provider, temperature=0.9)
700
- insight = response.get('response', '')
701
- if isinstance(insight, (list, dict)) or hasattr(insight, '__iter__') and not isinstance(insight, (str, bytes)):
702
- insight = ''.join([str(chunk) for chunk in insight])
703
-
704
- results.append({
705
- "combination_id": i+1,
706
- "facts": combo["facts"],
707
- "sources": combo["sources"],
708
- "emergent_insight": insight
709
- })
710
-
711
- return results
712
-
713
- def identify_patterns_across_chains(chains: Dict[str, List[str]], model: str = None, provider: str = None) -> Dict:
714
- """
715
- Identify meta-patterns across research chains, searching for higher-order insights.
716
-
717
- Args:
718
- chains: Dictionary mapping NPC names to their research chains
719
- model: LLM model to use
720
- provider: LLM provider to use
721
-
722
- Returns:
723
- Dictionary with meta-analysis results
724
- """
725
- # Prepare a summary of each research chain
726
- chain_summaries = {}
727
- for name, chain in chains.items():
728
- full_text = "\n\n".join(chain)
729
-
730
- summary_prompt = f"""
731
- Summarize the key themes, methodologies, and unusual perspectives in this research chain:
732
-
733
- {full_text[:2000]}...
734
-
735
- Focus on what makes this researcher's approach unique or valuable. Identify their core assumptions,
736
- methodological innovations, and blindspots (150-200 words).
737
- """
738
-
739
- response = get_llm_response(prompt=summary_prompt, model=model, provider=provider)
740
- summary = response.get('response', '')
741
- if isinstance(summary, (list, dict)) or hasattr(summary, '__iter__') and not isinstance(summary, (str, bytes)):
742
- summary = ''.join([str(chunk) for chunk in summary])
743
-
744
- chain_summaries[name] = summary
745
-
746
- # Generate meta-analysis across all chains
747
- all_summaries = "\n\n".join([f"[{name}]\n{summary}" for name, summary in chain_summaries.items()])
748
-
749
- meta_analysis_prompt = f"""
750
- Analyze these research approaches on the topic:
751
-
752
- {all_summaries}
753
-
754
- Identify:
755
- 1. Surprising methodological patterns - how are researchers approaching this problem in innovative ways?
756
- 2. Conceptual blindspots - what aspects seem to be collectively overlooked?
757
- 3. Emerging paradigms - are there new frameworks or models taking shape across multiple perspectives?
758
- 4. Productive tensions - where do disagreements or contradictions suggest valuable new research directions?
759
- 5. The topology of the problem space - how might we map the conceptual territory in a novel way?
760
-
761
- Focus on identifying higher-order insights that emerge from comparing these different approaches.
762
- Your analysis should challenge conventions and suggest new ways of framing the entire research domain.
763
- """
764
-
765
- response = get_llm_response(prompt=meta_analysis_prompt, model=model, provider=provider, temperature=0.8)
766
- meta_analysis = response.get('response', '')
767
- if isinstance(meta_analysis, (list, dict)) or hasattr(meta_analysis, '__iter__') and not isinstance(meta_analysis, (str, bytes)):
768
- meta_analysis = ''.join([str(chunk) for chunk in meta_analysis])
769
-
770
- # Generate innovative research directions
771
- directions_prompt = f"""
772
- Based on this meta-analysis of research approaches to the topic:
773
-
774
- {meta_analysis}
775
-
776
- Propose 5 highly innovative research directions that could transform this field.
777
- For each direction:
778
- 1. Frame a provocative research question
779
- 2. Explain why it's both important and neglected
780
- 3. Suggest an unconventional methodology to explore it
781
- 4. Describe what a breakthrough in this direction might look like
782
-
783
- Your suggestions should be bold, interdisciplinary, and challenge fundamental assumptions.
784
- Aim for directions that most researchers haven't considered but that could lead to significant advances.
785
- """
786
-
787
- response = get_llm_response(prompt=directions_prompt, model=model, provider=provider, temperature=0.9)
788
- new_directions = response.get('response', '')
789
- if isinstance(new_directions, (list, dict)) or hasattr(new_directions, '__iter__') and not isinstance(new_directions, (str, bytes)):
790
- new_directions = ''.join([str(chunk) for chunk in new_directions])
791
-
792
- return {
793
- "chain_summaries": chain_summaries,
794
- "meta_analysis": meta_analysis,
795
- "innovative_directions": new_directions
796
- }
797
-
798
- def preprocess_content_for_pdf(content: str, model: str = None, provider: str = None, max_words: int = 2000, concise_mode: bool = False) -> str:
799
- """
800
- Quick and lightweight preprocessing for PDF generation.
801
-
802
- Args:
803
- content: Raw content to preprocess
804
- model: LLM model to use (optional)
805
- provider: LLM provider to use (optional)
806
- max_words: Maximum word count (default 2000)
807
- concise_mode: If True, creates a very short summary instead of full formatting
808
-
809
- Returns:
810
- Formatted content ready for PDF generation
811
- """
812
- # Handle non-string content
813
- if not isinstance(content, str):
814
- content = str(content)
815
-
816
- # If in concise mode, create a drastically shortened version
817
- if concise_mode:
818
- from npcpy.llm_funcs import get_llm_response
819
- from npcpy.npc_sysenv import NPCSH_CHAT_MODEL, NPCSH_CHAT_PROVIDER
820
-
821
- if model is None:
822
- model = NPCSH_CHAT_MODEL
823
- if provider is None:
824
- provider = NPCSH_CHAT_PROVIDER
825
-
826
- concise_prompt = f"""
827
- Summarize the following content into an extremely concise, no-bullshit format with maximum 500 words:
828
- {content}
829
-
830
- - Use clear section headings
831
- - Use bullet points for key ideas
832
- - Focus only on essential insights
833
- - No verbose academic language
834
- - No padding or fillers
835
- - Just the core ideas in simple language
836
- """
837
-
838
- response = get_llm_response(prompt=concise_prompt, model=model, provider=provider)
839
- content = response.get('response', '')
840
-
841
- # Basic cleanup for any problematic characters that cause PDF issues
842
- for char, replacement in {
843
- '%': '',
844
- '#': '-',
845
- '_': '-',
846
- '~': '-',
847
- '^': '',
848
- '\\': '/',
849
- '{': '(',
850
- '}': ')'
851
- }.items():
852
- content = content.replace(char, replacement)
853
-
854
- # Apply word count limit if the content is too long
855
- words = content.split()
856
- if len(words) > max_words:
857
- content = ' '.join(words[:max_words]) + '... [truncated]'
858
-
859
- return content.strip()
860
-
861
- def generate_pdf_report(request: str,
862
- model,
863
- provider,
864
- research: Dict[str, Any],
865
- experiments: Dict[str, Dict[str, Any]],
866
- output_path: str = None,
867
- max_pages: int = 5) -> str:
868
- """
869
- Generate a professional PDF report using LaTeX for superior formatting, typesetting, and layout.
870
-
871
- Args:
872
- request: The original research question
873
- research: The consolidated research results
874
- experiments: The simulated experiments and their results
875
- output_path: Path to save the PDF report (default: current directory)
876
- fast_mode: If True, uses simpler formatting
877
- concise_mode: If True, drastically reduces content length
878
- max_pages: Maximum number of pages to generate (approximate)
879
-
880
- Returns:
881
- Path to the generated PDF file
882
- """
883
- if output_path is None:
884
- output_path = os.getcwd()
885
-
886
- # Create filename
887
- sanitized_request = "".join(c for c in request if c.isalnum() or c.isspace()).strip()
888
- sanitized_request = sanitized_request.replace(" ", "_")[:50]
889
- timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
890
- filename = f"{sanitized_request}_{timestamp}"
891
-
892
- # Check for LaTeX installation
893
- try:
894
- subprocess.run(["which", "pdflatex"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
895
- except subprocess.CalledProcessError:
896
- print("LaTeX not installed. Attempting to install...")
897
- try:
898
- subprocess.run(["apt-get", "update"], check=True)
899
- subprocess.run(["apt-get", "install", "-y", "texlive-latex-base", "texlive-fonts-recommended",
900
- "texlive-fonts-extra", "texlive-latex-extra"], check=True)
901
- except subprocess.CalledProcessError as e:
902
- print(f"Error installing LaTeX: {str(e)}")
903
- return None
904
- # Create chart for thematic groups using matplotlib
905
- chart_path = None
906
- try:
907
- if "group_evaluations" in research and research["group_evaluations"]:
908
- # Create basic folder for figures
909
- figures_dir = os.path.join(output_path, "figures")
910
- os.makedirs(figures_dir, exist_ok=True)
911
-
912
- fig, ax = plt.subplots(figsize=(7.5, 4))
913
- plt.style.use('ggplot') # Clean style without seaborn
914
-
915
- groups = []
916
- scores = []
917
-
918
- for group_name, eval_data in research["group_evaluations"].items():
919
- groups.append(group_name[:30]) # Truncate long names
920
- quality_score = (eval_data.get("Novelty", 5) + eval_data.get("Depth", 5) +
921
- eval_data.get("Practicality", 5) + eval_data.get("Evidence", 5)) / 4
922
- scores.append(quality_score)
923
-
924
- # Sort by score
925
- sorted_data = sorted(zip(groups, scores), key=lambda x: x[1], reverse=True)
926
- groups = [x[0] for x in sorted_data]
927
- scores = [x[1] for x in sorted_data]
928
-
929
- # Create horizontal bar chart
930
- y_pos = range(len(groups))
931
- ax.barh(y_pos, scores, color='steelblue')
932
- ax.set_yticks(y_pos)
933
- ax.set_yticklabels(groups)
934
- ax.set_xlabel('Quality Score (1-10)')
935
- ax.set_title('Thematic Groups by Quality Score')
936
- plt.tight_layout()
937
-
938
- # Save chart
939
- chart_path = os.path.join(figures_dir, f"thematic_groups.pdf")
940
- plt.savefig(chart_path, dpi=300, bbox_inches='tight', format='pdf')
941
- plt.close()
942
- except Exception as e:
943
- print(f"Warning: Could not generate chart: {str(e)}")
944
-
945
- # Create LaTeX document
946
- latex_content = generate_latex_document(request, model, provider, research, experiments, chart_path, max_pages)
947
-
948
- # Write LaTeX to file
949
- tex_path = os.path.join(output_path, f"{filename}.tex")
950
- with open(tex_path, "w") as f:
951
- f.write(latex_content)
952
-
953
- # Use subprocess to run pdflatex without check=True to prevent exceptions
954
- try:
955
- # First run
956
- result = subprocess.run(
957
- ["pdflatex", "-interaction=nonstopmode", "-output-directory", output_path, tex_path],
958
- stdout=subprocess.PIPE,
959
- stderr=subprocess.PIPE
960
- )
961
-
962
- if result.returncode != 0:
963
- print(f"Warning: First LaTeX run had issues (exit code {result.returncode})")
964
- # Still continue - sometimes the second run fixes things
965
-
966
- # Second run for references
967
- result = subprocess.run(
968
- ["pdflatex", "-interaction=nonstopmode", "-output-directory", output_path, tex_path],
969
- stdout=subprocess.PIPE,
970
- stderr=subprocess.PIPE
971
- )
972
-
973
- if result.returncode != 0:
974
- print(f"Warning: Second LaTeX run had issues (exit code {result.returncode})")
975
- # Write LaTeX log for debugging
976
- log_path = os.path.join(output_path, f"{filename}.log")
977
- if os.path.exists(log_path):
978
- print(f"Check LaTeX log for details: {log_path}")
979
- except Exception as e:
980
- print(f"Error during LaTeX compilation: {str(e)}")
981
- return None
982
-
983
- # Clean up temporary files
984
- for ext in [".aux", ".out", ".toc"]:
985
- try:
986
- os.remove(os.path.join(output_path, f"{filename}{ext}"))
987
- except OSError:
988
- pass
989
-
990
- # Check if PDF was generated successfully
991
- pdf_path = os.path.join(output_path, f"{filename}.pdf")
992
- if os.path.exists(pdf_path):
993
- print(f"PDF report successfully generated using LaTeX: {pdf_path}")
994
- return pdf_path
995
- else:
996
- print(f"PDF generation failed. Check the LaTeX log for details.")
997
- return None
998
-
999
- def generate_latex_document(request: str, model, provider, research: Dict[str, Any], experiments: Dict[str, Dict[str, Any]],
1000
- chart_path: str = None, max_pages: int = 5) -> str:
1001
- """
1002
- Generate LaTeX document content.
1003
-
1004
- Args:
1005
- request: The research topic
1006
- research: Research results
1007
- experiments: Experiments data
1008
- chart_path: Path to the thematic groups chart
1009
- max_pages: Maximum number of pages (approximate)
1010
-
1011
- Returns:
1012
- LaTeX document content as a string
1013
- """
1014
- # Collect experiment images that might be available
1015
- figure_paths = {}
1016
- if chart_path:
1017
- # Use relative path instead of absolute path for figure
1018
- figure_paths["thematic_groups"] = os.path.basename(chart_path)
1019
-
1020
- # Check for experiment images in the current directory
1021
- # Ensure experiments is a dictionary before trying to get keys
1022
- if isinstance(experiments, dict):
1023
- for title in experiments.keys():
1024
- sanitized_title = title.replace(" ", "_")
1025
- potential_image = f"{sanitized_title}_experiment.png"
1026
- if os.path.exists(potential_image):
1027
- figure_paths[sanitized_title] = potential_image
1028
-
1029
- # Describe available figures to the LLM
1030
- figure_path_description_dict = {}
1031
- for name, path in figure_paths.items():
1032
- figure_path_description_dict[name] = path
1033
-
1034
- # Create the prompt for generating LaTeX content
1035
- prompt = f'''
1036
- Generate a LaTeX document for a research report on the topic: "{request}"
1037
- Here is the summary of the research: {research}
1038
-
1039
- Here is the summary of the experiments: {experiments}''' +"""
1040
- Write your response in a way that academically details the research, its motivation, and experiments
1041
- and ensure any place where a citation may be needed is indicated by including an empty '\\cite{citation_needed}'
1042
-
1043
- IMPORTANT INSTRUCTIONS FOR DOCUMENT PREPARATION:
1044
- 1. DO NOT include \\bibliography{references} or any bibliography commands, as we don't have a references file
1045
- 2. Instead, create a \\begin{thebibliography}{99} ... \\end{thebibliography} section with example references
1046
- 3. For figures, use relative paths like 'figures/thematic_groups.pdf' rather than absolute paths
1047
- 4. Make sure all LaTeX commands are properly formatted and do not use undefined packages
1048
- 5. Keep the document structure simple and robust to avoid compilation errors
1049
- """+f"""
1050
- The figures are located at the following paths: {figure_path_description_dict}
1051
- """
1052
-
1053
-
1054
- latex_response = get_llm_response(prompt=prompt, model=model, provider=provider )
1055
- latex_content = latex_response.get('response', '')
1056
-
1057
- # Post-process the LaTeX content to fix common issues
1058
- latex_content = latex_content.replace('\\bibliography{references}', '')
1059
- latex_content = latex_content.replace('\\bibliographystyle{plain}', '')
1060
-
1061
- # Replace absolute figure paths with relative paths
1062
- latex_content = latex_content.replace('/home/caug/npcww/npcsh/figures/', 'figures/')
1063
-
1064
- # Add a simple bibliography if none exists
1065
- if '\\begin{thebibliography}' not in latex_content and '\\end{document}' in latex_content:
1066
- bibliography = """
1067
- \\begin{thebibliography}{9}
1068
- \\bibitem{citation1} Author, A. (2023). Title of the work. Journal Name, 10(2), 123-456.
1069
- \\bibitem{citation2} Researcher, B. (2022). Another relevant publication. Conference Proceedings, 789-012.
1070
- \\end{thebibliography}
1071
- """
1072
- latex_content = latex_content.replace('\\end{document}', f'{bibliography}\n\\end{{document}}')
1073
-
1074
- return latex_content
1075
-