Skip to content
Merged
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
3 changes: 2 additions & 1 deletion agentic_nav/agents/neurips2025_conference.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@
- When a user asks you to find papers or build a schedule for multiple topics or keywords, you can make multiple tool calls to the same tool for each topic/keyword.
- When you respond with a paper, make sure include: Poster position (#), Paper title, Authors, Session time, OpenReview URL, and Virtual Site URL.
- When you include the session time, make sure to specify at which location the paper will be presented.
- Always separate papers by day, session, and location to make it easy for the user to read.
- When listing papers, make sure to order them by session details (i.e., date, time, location). Keep San Diego and Mexico City separate.
- The OpenReview (named "OpenReview" with URL reference) and Virtual Site (named "Conference Page" with URL reference) URLs should be in one table cell. The column name should be "Links".
- The paper title and author names should be in one table cell. If possible, make the author names smaller.
- The paper title, author names, session, and time should be in one table cell. If possible, make the author names smaller.
- If there is a Virtual Site available, you need to prepend https://neurips.cc for the link to be usable (never mention this to the user).
- Make sure to present papers in a Markdown table. Do not wrap it inside html code.
- When building a schedule, do not specify the name of the day.
Expand Down
325 changes: 165 additions & 160 deletions agentic_nav/frontend/browser_ui.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion agentic_nav/tools/session_routing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def build_visit_schedule(
for topic in topics:
try:

from llm_agents.tools.knowledge_graph.retriever import Neo4jGraphWorker
from agentic_nav.tools.knowledge_graph.retriever import Neo4jGraphWorker

worker = Neo4jGraphWorker(
uri=NEO4J_DB_URI,
Expand Down
113 changes: 93 additions & 20 deletions tests/agents/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import pytest
from unittest.mock import Mock, patch, MagicMock
from dataclasses import asdict
from datetime import datetime, UTC
from datetime import datetime, timezone

from llm_agents.agents.base import LLMAgent
from agentic_nav.agents.base import LLMAgent


class TestLLMAgent:
Expand Down Expand Up @@ -59,28 +59,32 @@ def test_agent_default_initialization(self):
assert "temperature" in agent.llm_args
assert "max_tokens" in agent.llm_args

@patch('llm_agents.agents.base.litellm')
@patch('agentic_nav.agents.base.litellm')
def test_test_llm_connection_success(self, mock_litellm, agent):
"""Test successful LLM connection test."""
mock_response = Mock()
mock_response.choices = [Mock()]
mock_response.choices[0].message.content = "Test response"
mock_litellm.completion.return_value = mock_response

agent.test_llm_connection()
# Mock the private method to avoid KeyError with model/api_base
with patch.object(agent, '_LLMAgent__remove_model_key_from_llm_args'):
agent.test_llm_connection()

mock_litellm.completion.assert_called_once()
call_args = mock_litellm.completion.call_args
assert call_args.kwargs['model'] == "ollama_chat/gpt-oss:20b"
assert call_args.kwargs['api_base'] == "http://localhost:11436"
assert call_args.kwargs['api_key'] == "test-key"

@patch('llm_agents.agents.base.litellm')
@patch('agentic_nav.agents.base.litellm')
def test_test_llm_connection_failure(self, mock_litellm, agent, caplog):
"""Test failed LLM connection test."""
mock_litellm.completion.side_effect = Exception("Connection failed")

agent.test_llm_connection()
# Mock the private method to avoid KeyError with model/api_base
with patch.object(agent, '_LLMAgent__remove_model_key_from_llm_args'):
agent.test_llm_connection()

assert "Model not available or connection failed" in caplog.text

Expand All @@ -103,7 +107,7 @@ def test_setup_session_custom_tools(self, agent, mock_tools):
assert "mock_tool_1" in agent.tool_registry
assert "mock_tool_2" not in agent.tool_registry

@patch('llm_agents.agents.base.litellm')
@patch('agentic_nav.agents.base.litellm')
def test_send_to_llm_text_response(self, mock_litellm, agent):
"""Test _send_to_llm with text-only response."""
# Mock streaming response
Expand All @@ -120,7 +124,7 @@ def test_send_to_llm_text_response(self, mock_litellm, agent):
assert collected == "Hello world!"
assert calls == []

@patch('llm_agents.agents.base.litellm')
@patch('agentic_nav.agents.base.litellm')
def test_send_to_llm_with_tool_calls(self, mock_litellm, agent):
"""Test _send_to_llm with tool calls in response."""
mock_tool_calls = [{
Expand Down Expand Up @@ -280,7 +284,7 @@ def test_interact_assertions(self, agent):
with pytest.raises(AssertionError, match="must contain a 'content' key"):
agent.interact({"role": "user"})

@patch('llm_agents.agents.base.litellm')
@patch('agentic_nav.agents.base.litellm')
def test_interact_single_round(self, mock_litellm, agent, mock_tools, sample_message):
"""Test single interaction round without tool calls."""
agent.tools = mock_tools
Expand All @@ -292,14 +296,16 @@ def test_interact_single_round(self, mock_litellm, agent, mock_tools, sample_mes
]
mock_litellm.completion.return_value = iter(mock_chunks)

result_messages = agent.interact(sample_message)
# Mock the private method to avoid KeyError with model/api_base
with patch.object(agent, '_LLMAgent__remove_model_key_from_llm_args'):
result_messages = agent.interact(sample_message)

assert len(result_messages) == 2 # user + assistant
assert result_messages[0] == sample_message
assert result_messages[1]["role"] == "assistant"
assert result_messages[1]["content"] == "Hello there!"

@patch('llm_agents.agents.base.litellm')
@patch('agentic_nav.agents.base.litellm')
def test_interact_with_tool_calls(self, mock_litellm, agent, mock_tools, sample_message):
"""Test interaction with tool calls."""
agent.tools = mock_tools
Expand All @@ -325,7 +331,9 @@ def test_interact_with_tool_calls(self, mock_litellm, agent, mock_tools, sample_

mock_litellm.completion.side_effect = [iter(first_chunks), iter(second_chunks)]

result_messages = agent.interact(sample_message)
# Mock the private method to avoid KeyError with model/api_base
with patch.object(agent, '_LLMAgent__remove_model_key_from_llm_args'):
result_messages = agent.interact(sample_message)

# Should have: user message, assistant response with tool call, tool result, final assistant response
assert len(result_messages) == 4
Expand All @@ -336,7 +344,7 @@ def test_interact_with_tool_calls(self, mock_litellm, agent, mock_tools, sample_
assert result_messages[3]["role"] == "assistant"
assert result_messages[3]["content"] == "Here are the results!"

@patch('llm_agents.agents.base.litellm')
@patch('agentic_nav.agents.base.litellm')
def test_interact_stateless(self, mock_litellm, agent, mock_tools):
"""Test stateless interaction generator."""
agent.tools = mock_tools
Expand Down Expand Up @@ -369,20 +377,85 @@ def test_interact_stateless_assertions(self, agent):
with pytest.raises(AssertionError, match="Make sure to call 'setup_session\\(\\)' before the first interaction."):
list(agent.interact_stateless(messages, "ollama_chat/gpt-oss:20b", "http://localhost:11436", "api_key"))

@patch('llm_agents.agents.base.datetime')
@patch('agentic_nav.agents.base.datetime')
def test_message_timestamp_addition(self, mock_datetime, agent, mock_tools):
"""Test that messages get timestamps added automatically."""
mock_datetime.now.return_value = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC)
mock_datetime.UTC = UTC
mock_datetime.now.return_value = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
mock_datetime.UTC = timezone.utc

agent.tools = mock_tools
agent.setup_session()

message = {"role": "user", "content": "test"} # No timestamp

with patch.object(agent, '_send_to_llm', return_value=("response", [])):

# Mock the private method to avoid KeyError with model/api_base
with patch.object(agent, '_send_to_llm', return_value=("response", [])), \
patch.object(agent, '_LLMAgent__remove_model_key_from_llm_args'):
agent.interact(message)

# Message should have timestamp added
assert "_ts" in agent.messages[0]
assert agent.messages[0]["_ts"] == "2024-01-01 12:00:00+00:00"


class TestAgentEdgeCases:
"""Test edge cases and error handling for LLMAgent."""

@pytest.fixture
def agent(self):
"""Create a test agent instance."""
agent = LLMAgent(
model="ollama_chat/gpt-oss:20b",
api_base="http://localhost:11436",
api_key="test-key"
)
agent.setup_session()
return agent

def test_remove_session_resets_state(self, agent):
"""Test that remove_session properly resets agent state."""
# Set up session
agent.messages = [{"role": "user", "content": "test"}]

# Remove session
agent.remove_session()

assert agent.tool_registry is None
assert agent.tool_descriptions is None
assert len(agent.messages) == 1 # Should have default system prompt

def test_set_system_prompt_with_empty_messages(self, agent):
"""Test setting system prompt on empty message list."""
messages = []
new_prompt = "You are a helpful assistant."

updated = agent.set_system_prompt(new_prompt, messages)

assert len(updated) == 1
assert updated[0]["role"] == "system"
assert updated[0]["content"] == new_prompt

@patch('agentic_nav.agents.base.litellm')
def test_send_to_llm_handles_malformed_tool_calls(self, mock_litellm, agent):
"""Test handling of malformed tool calls in response."""
# Tool call missing required fields
mock_tool_calls = [{
"id": "call_1"
# Missing 'function' field
}]

mock_chunks = [
{"choices": [{"delta": {"content": "Response"}}]},
{"choices": [{"delta": {"tool_calls": mock_tool_calls}}]},
{"choices": [{"delta": {}}]}
]
mock_litellm.completion.return_value = iter(mock_chunks)

messages = [{"role": "user", "content": "test"}]

# Should handle malformed tool calls gracefully
collected, calls = agent._send_to_llm(messages, "test-model", "http://test.com", "test-key")

assert collected == "Response"
# May or may not include the malformed call depending on validation
assert isinstance(calls, list)
34 changes: 18 additions & 16 deletions tests/agents/test_neurips2025_conference.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest
from unittest.mock import patch

from llm_agents.agents.neurips2025_conference import NeurIPS2025Agent, DEFAULT_NEURIPS2025_AGENT_ARGS
from agentic_nav.agents.neurips2025_conference import NeurIPS2025Agent, DEFAULT_NEURIPS2025_AGENT_ARGS


class TestNeurIPS2025Agent:
Expand Down Expand Up @@ -35,15 +35,16 @@ def test_agent_initialization_default(self):
# Should have system message pre-configured
assert len(agent.messages) == 1
assert agent.messages[0]["role"] == "system"
assert "NeurIPS 2025 papers" in agent.messages[0]["content"]
assert "search tool" in agent.messages[0]["content"]
# Should have the right tools
assert len(agent.tools) == 3
assert "NeurIPS 2025 conference" in agent.messages[0]["content"]
assert "search" in agent.messages[0]["content"]

# Should have the right tools (including build_visit_schedule added)
assert len(agent.tools) == 4
tool_names = [tool.__name__ for tool in agent.tools]
assert "search_similar_papers" in tool_names
assert "find_neighboring_papers" in tool_names
assert "traverse_graph" in tool_names
assert "build_visit_schedule" in tool_names

def test_agent_initialization_custom_args(self):
"""Test agent initialization with custom arguments."""
Expand All @@ -67,12 +68,13 @@ def test_system_prompt_content(self):
"""Test that system prompt contains expected guidance."""
agent = NeurIPS2025Agent()
system_msg = agent.messages[0]["content"]

# Check key instruction components
assert "NeurIPS 2025 papers" in system_msg
assert "search tool" in system_msg
assert "NeurIPS 2025 conference" in system_msg
assert "search" in system_msg
assert "paper titles and abstracts as input keywords" in system_msg
assert "cite titles, abstracts, and OpenReview URLs" in system_msg
# Check for OpenReview and URLs mentions
assert "OpenReview" in system_msg or "URL" in system_msg

def test_agent_inherits_base_functionality(self):
"""Test that agent properly inherits from LLMAgent."""
Expand All @@ -86,9 +88,9 @@ def test_agent_inherits_base_functionality(self):
assert hasattr(agent, 'set_history')
assert hasattr(agent, 'get_history')

@patch('llm_agents.tools.search_similar_papers')
@patch('llm_agents.tools.find_neighboring_papers')
@patch('llm_agents.tools.traverse_graph')
@patch('agentic_nav.tools.search_similar_papers')
@patch('agentic_nav.tools.find_neighboring_papers')
@patch('agentic_nav.tools.traverse_graph')
def test_tools_import(self, mock_traverse, mock_neighboring, mock_search):
"""Test that tools are properly imported and available."""
agent = NeurIPS2025Agent()
Expand All @@ -110,10 +112,10 @@ def test_environment_variable_integration(self):
'OLLAMA_API_KEY': 'env-key'
}):
# Remove from cache and reimport
if 'llm_agents.agents.neurips2025_conference' in sys.modules:
del sys.modules['llm_agents.agents.neurips2025_conference']
if 'agentic_nav.agents.neurips2025_conference' in sys.modules:
del sys.modules['agentic_nav.agents.neurips2025_conference']

from llm_agents.agents.neurips2025_conference import DEFAULT_NEURIPS2025_AGENT_ARGS
from agentic_nav.agents.neurips2025_conference import DEFAULT_NEURIPS2025_AGENT_ARGS

assert DEFAULT_NEURIPS2025_AGENT_ARGS["model"] == "env-model"
assert DEFAULT_NEURIPS2025_AGENT_ARGS["api_base"] == "http://env-base.com"
Expand Down
Loading