Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
OPENAI_API_KEY=
GOOGLE_API_KEY=
ANTHROPIC_API_KEY=
PERPLEXITY_API_KEY=
SEMANTIC_SCHOLAR_KEY=
FUTURE_HOUSE_API_KEY=
OPENROUTER_API_KEY=
59 changes: 56 additions & 3 deletions denario/denario.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def __init__(self,
if project_dir is None:
project_dir = os.path.join( os.getcwd(), DEFAUL_PROJECT_NAME )
if not os.path.exists(project_dir):
os.mkdir(project_dir)
os.makedirs(project_dir, exist_ok=True)

if research is None:
research = Research() # Initialize with default values
Expand All @@ -56,12 +56,12 @@ def __init__(self,
os.makedirs(project_dir, exist_ok=True)
self.project_dir = project_dir

self._setup_input_files()

self.plots_folder = os.path.join(self.project_dir, INPUT_FILES, PLOTS_FOLDER)
# Ensure the folder exists
os.makedirs(self.plots_folder, exist_ok=True)

self._setup_input_files()

# Get keys from environment if they exist
self.keys = KeyManager()
self.keys.get_keys_from_env()
Expand Down Expand Up @@ -905,6 +905,59 @@ def referee(self,
except FileNotFoundError as e:
print('Denario failed to provide a review for the paper. Ensure that a paper in the `paper` folder ex')
print(f'Error: {e}')

def add_literature(self, sections: list[str] = ["idea", "methods", "results"], n_paragraphs: int = None) -> None:
"""
Add literature references to the specified sections.

Args:
sections: list of sections to add citations to (choose from "idea", "method", "results").
n_paragraphs: maximum number of paragraphs to process per section.
"""
from .paper_agents.literature import process_tex_file_with_references

# Start message
print(f"--- Adding Literature References to {', '.join(sections)} ---")

# Mapping between section names and Research attributes
attr_map = {"idea": "idea", "methods": "methodology", "results": "results"}

all_bibs = ""
for section in sections:
# Check if section exists in research object
attr_name = attr_map.get(section, section)
content = getattr(self.research, attr_name, None)

if not content:
print(f"Skipping {section} as it is empty or not found.")
continue

print(f"Processing {section}...")
new_content, bib = process_tex_file_with_references(content, self.keys, nparagraphs=n_paragraphs)

# Update internal state
setattr(self.research, attr_name, new_content)

# Accumulate bibliography
if bib:
all_bibs += bib.strip() + "\n\n"

# Save the updated section back to input_files
file_map = {"idea": IDEA_FILE, "methods": METHOD_FILE, "results": RESULTS_FILE}
if section in file_map:
save_path = os.path.join(self.project_dir, INPUT_FILES, file_map[section])
with open(save_path, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f" - Updated {file_map[section]}")

# Save combined bibliography to input_files/bibliography.bib
if all_bibs:
bib_path = os.path.join(self.project_dir, INPUT_FILES, "bibliography.bib")
with open(bib_path, 'w', encoding='utf-8') as f:
f.write(all_bibs.strip())
print(f" - Generated bibliography.bib")

print("[SUCCESS] Literature references added.")

def research_pilot(self, data_description: str | None = None) -> None:
"""Full run of Denario. It calls the following methods sequentially:
Expand Down
101 changes: 79 additions & 22 deletions denario/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .key_manager import KeyManager
from .prompts.experiment import experiment_planner_prompt, experiment_engineer_prompt, experiment_researcher_prompt
from .utils import create_work_dir, get_task_result
from .openrouter_config import build_cmbagent_api_keys

class Experiment:
"""
Expand Down Expand Up @@ -79,36 +80,92 @@ def run_experiment(self, data_description: str, **kwargs):
print(f"Restart at step: {self.restart_at_step}")
print(f"Hardware constraints: {self.hardware_constraints}")

results = cmbagent.planning_and_control_context_carryover(data_description,
n_plan_reviews = 1,
max_n_attempts = self.max_n_attempts,
max_plan_steps = self.max_n_steps,
max_rounds_control = 500,
engineer_model = self.engineer_model,
researcher_model = self.researcher_model,
planner_model = self.planner_model,
plan_reviewer_model = self.plan_reviewer_model,
plan_instructions=self.planner_append_instructions,
researcher_instructions=self.researcher_append_instructions,
engineer_instructions=self.engineer_append_instructions,
work_dir = self.experiment_dir,
api_keys = self.api_keys,
restart_at_step = self.restart_at_step,
hardware_constraints = self.hardware_constraints,
default_llm_model = self.orchestration_model,
default_formatter_model = self.formatter_model
)
# Check if using OpenRouter (OPENROUTER_API_KEY is set)
import os
import cmbagent.utils as cmbagent_utils

use_openrouter = bool(os.getenv("OPENROUTER_API_KEY"))
original_get_model_config = None

if use_openrouter:
print("Using OpenRouter for all LLM calls...")

# Monkey-patch cmbagent's get_model_config to use OpenRouter
original_get_model_config = cmbagent_utils.get_model_config

def openrouter_get_model_config(model, api_keys):
"""Patched get_model_config that routes through OpenRouter."""
from denario.openrouter_config import build_openrouter_config
return build_openrouter_config(model)

cmbagent_utils.get_model_config = openrouter_get_model_config

# Also patch the module-level reference in cmbagent.cmbagent
import cmbagent.cmbagent as cmbagent_mod
cmbagent_mod.get_model_config = openrouter_get_model_config

api_keys = build_cmbagent_api_keys()
else:
api_keys = self.api_keys

try:
results = cmbagent.planning_and_control_context_carryover(data_description,
n_plan_reviews = 1,
max_n_attempts = self.max_n_attempts,
max_plan_steps = self.max_n_steps,
max_rounds_control = 500,
engineer_model = self.engineer_model,
researcher_model = self.researcher_model,
planner_model = self.planner_model,
plan_reviewer_model = self.plan_reviewer_model,
plan_instructions=self.planner_append_instructions,
researcher_instructions=self.researcher_append_instructions,
engineer_instructions=self.engineer_append_instructions,
work_dir = self.experiment_dir,
api_keys = api_keys,
restart_at_step = self.restart_at_step,
hardware_constraints = self.hardware_constraints,
default_llm_model = self.orchestration_model,
default_formatter_model = self.formatter_model,
)
finally:
# Restore original function after call
if original_get_model_config is not None:
cmbagent_utils.get_model_config = original_get_model_config
import cmbagent.cmbagent as cmbagent_mod
cmbagent_mod.get_model_config = original_get_model_config
chat_history = results['chat_history']
final_context = results['final_context']

try:
task_result = get_task_result(chat_history,'researcher_response_formatter')
# Try to get the result from the researcher formatter first
task_result = get_task_result(chat_history, 'researcher_response_formatter')

# Fallback to executor formatter if researcher result is missing
if task_result is None:
print("Researcher result not found, falling back to executor_response_formatter...")
task_result = get_task_result(chat_history, 'executor_response_formatter')

if task_result is None:
raise ValueError("Could not find result in chat history from any formatter (researcher or executor).")

except Exception as e:
# If we reached this point, we really don't have the task result
raise e

MD_CODE_BLOCK_PATTERN = r"```[ \t]*(?:markdown)[ \t]*\r?\n(.*)\r?\n[ \t]*```"
extracted_results = re.findall(MD_CODE_BLOCK_PATTERN, task_result, flags=re.DOTALL)[0]
clean_results = re.sub(r'^<!--.*?-->\s*\n', '', extracted_results)
matches = re.findall(MD_CODE_BLOCK_PATTERN, task_result, flags=re.DOTALL)
if not matches:
# If no markdown block found in task_result, maybe the whole string is the result or it's formatted differently
# Try a simpler extraction or use the whole string
if "##" in task_result:
clean_results = re.sub(r'^<!--.*?-->\s*\n', '', task_result)
else:
raise ValueError("Could not find markdown content in the task result.")
else:
extracted_results = matches[0]
clean_results = re.sub(r'^<!--.*?-->\s*\n', '', extracted_results)

self.results = clean_results
self.plot_paths = final_context['displayed_images']

Expand Down
2 changes: 2 additions & 0 deletions denario/key_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class KeyManager(BaseModel):
OPENAI: str | None = ""
PERPLEXITY: str | None = ""
SEMANTIC_SCHOLAR: str | None = ""
OPENROUTER: str | None = ""

def get_keys_from_env(self) -> None:

Expand All @@ -18,6 +19,7 @@ def get_keys_from_env(self) -> None:
self.ANTHROPIC = os.getenv("ANTHROPIC_API_KEY") #not strictly needed
self.PERPLEXITY = os.getenv("PERPLEXITY_API_KEY") #only for citations
self.SEMANTIC_SCHOLAR = os.getenv("SEMANTIC_SCHOLAR_KEY") #only for fast semantic scholar
self.OPENROUTER = os.getenv("OPENROUTER_API_KEY")

def __getitem__(self, key: str) -> str:
return getattr(self, key)
Expand Down
15 changes: 14 additions & 1 deletion denario/langgraph_agents/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from .parameters import GraphState
from ..config import INPUT_FILES, IDEA_FILE, METHOD_FILE, LITERATURE_FILE, REFEREE_FILE, PAPER_FOLDER
from ..llm import models

def preprocess_node(state: GraphState, config: RunnableConfig):
"""
Expand All @@ -17,7 +18,19 @@ def preprocess_node(state: GraphState, config: RunnableConfig):

#########################################
# set the LLM
if 'gemini' in state['llm']['model']:
if state["keys"].OPENROUTER:
model_name = state['llm']['model']
openrouter_model = model_name
if model_name in models:
if models[model_name].openrouter_name:
openrouter_model = models[model_name].openrouter_name

state['llm']['llm'] = ChatOpenAI(model=openrouter_model,
temperature=state['llm']['temperature'],
openai_api_key=state["keys"].OPENROUTER,
base_url="https://openrouter.ai/api/v1")

elif 'gemini' in state['llm']['model']:
state['llm']['llm'] = ChatGoogleGenerativeAI(model=state['llm']['model'],
temperature=state['llm']['temperature'],
google_api_key=state["keys"].GEMINI)
Expand Down
58 changes: 44 additions & 14 deletions denario/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,81 +9,111 @@ class LLM(BaseModel):
"""Maximum output tokens allowed."""
temperature: float | None
"""Temperature of the model."""
openrouter_name: str | None = None
"""OpenRouter model ID."""

gemini20flash = LLM(name="gemini-2.0-flash",
max_output_tokens=8192,
temperature=0.7)
temperature=0.7,
openrouter_name="google/gemini-2.0-flash-exp:free")
"""`gemini-2.0-flash` model."""

gemini25flash = LLM(name="gemini-2.5-flash",
max_output_tokens=65536,
temperature=0.7)
temperature=0.7,
openrouter_name="google/gemini-2.0-flash-exp:free")
"""`gemini-2.5-flash` model."""

gemini25pro = LLM(name="gemini-2.5-pro",
max_output_tokens=65536,
temperature=0.7)
temperature=0.7,
openrouter_name="google/gemini-2.0-pro-exp-02-05:free")
"""`gemini-2.5-pro` model."""

gemini3pro_preview = LLM(name="gemini-3-pro-preview",
max_output_tokens=65536,
temperature=0.7,
openrouter_name="google/gemini-3-pro-preview")
"""`gemini-3-pro-preview` model."""

gemini3flash_preview = LLM(name="gemini-3-flash-preview",
max_output_tokens=65536,
temperature=0.7,
openrouter_name="google/gemini-3-flash-preview")
"""`gemini-3-flash-preview` model."""

o3mini = LLM(name="o3-mini-2025-01-31",
max_output_tokens=100000,
temperature=None)
temperature=None,
openrouter_name="openai/o3-mini")
"""`o3-mini` model."""

gpt4o = LLM(name="gpt-4o-2024-11-20",
max_output_tokens=16384,
temperature=0.5)
temperature=0.5,
openrouter_name="openai/gpt-4o")
"""`gpt-4o` model."""

gpt41 = LLM(name="gpt-4.1-2025-04-14",
max_output_tokens=16384,
temperature=0.5)
temperature=0.5,
openrouter_name="openai/gpt-4-turbo")
"""`gpt-4.1` model."""

gpt41mini = LLM(name="gpt-4.1-mini",
max_output_tokens=16384,
temperature=0.5)
temperature=0.5,
openrouter_name="openai/gpt-4-turbo-preview")
"""`gpt-4.1-mini` model."""

gpt4omini = LLM(name="gpt-4o-mini-2024-07-18",
max_output_tokens=16384,
temperature=0.5)
temperature=0.5,
openrouter_name="openai/gpt-4o-mini")
"""`gpt-4o-mini` model."""

gpt45 = LLM(name="gpt-4.5-preview-2025-02-27",
max_output_tokens=16384,
temperature=0.5)
temperature=0.5,
openrouter_name="openai/gpt-4.5-preview")
"""`gpt-4.5-preview` model."""

gpt5 = LLM(name="gpt-5",
max_output_tokens=128000,
temperature=None)
temperature=None,
openrouter_name="openai/gpt-5")
"""`gpt-5` model """

gpt5mini = LLM(name="gpt-5-mini",
max_output_tokens=128000,
temperature=None)
temperature=None,
openrouter_name="openai/gpt-5")
"""`gpt-5-mini` model."""

claude37sonnet = LLM(name="claude-3-7-sonnet-20250219",
max_output_tokens=64000,
temperature=0)
temperature=0,
openrouter_name="anthropic/claude-3.7-sonnet")
"""`claude-3-7-sonnet` model."""

claude4opus = LLM(name="claude-opus-4-20250514",
max_output_tokens=32000,
temperature=0)
temperature=0,
openrouter_name="anthropic/claude-3-opus")
"""`claude-4-Opus` model."""

claude41opus = LLM(name="claude-opus-4-1-20250805",
max_output_tokens=32000,
temperature=0)
temperature=0,
openrouter_name="anthropic/claude-3-opus")
"""`claude-4.1-Opus` model."""

models : Dict[str, LLM] = {
"gemini-2.0-flash" : gemini20flash,
"gemini-2.5-flash" : gemini25flash,
"gemini-2.5-pro" : gemini25pro,
"gemini-3-pro-preview" : gemini3pro_preview,
"gemini-3-flash-preview" : gemini3flash_preview,
"o3-mini" : o3mini,
"gpt-4o" : gpt4o,
"gpt-4.1" : gpt41,
Expand Down
Loading