aiagents4pharma 1.9.0__py3-none-any.whl → 1.15.0__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 (66) hide show
  1. aiagents4pharma/__init__.py +9 -6
  2. aiagents4pharma/configs/config.yaml +2 -1
  3. aiagents4pharma/configs/talk2biomodels/__init__.py +1 -0
  4. aiagents4pharma/configs/talk2biomodels/agents/t2b_agent/default.yaml +9 -3
  5. aiagents4pharma/configs/talk2biomodels/tools/__init__.py +4 -0
  6. aiagents4pharma/configs/talk2biomodels/tools/ask_question/__init__.py +3 -0
  7. aiagents4pharma/talk2biomodels/__init__.py +1 -0
  8. aiagents4pharma/talk2biomodels/agents/t2b_agent.py +14 -11
  9. aiagents4pharma/talk2biomodels/api/__init__.py +6 -0
  10. aiagents4pharma/talk2biomodels/api/kegg.py +83 -0
  11. aiagents4pharma/talk2biomodels/api/ols.py +72 -0
  12. aiagents4pharma/talk2biomodels/api/uniprot.py +35 -0
  13. aiagents4pharma/talk2biomodels/models/basico_model.py +29 -32
  14. aiagents4pharma/talk2biomodels/models/sys_bio_model.py +9 -6
  15. aiagents4pharma/talk2biomodels/states/state_talk2biomodels.py +24 -7
  16. aiagents4pharma/talk2biomodels/tests/test_api.py +57 -0
  17. aiagents4pharma/talk2biomodels/tests/test_ask_question.py +44 -0
  18. aiagents4pharma/talk2biomodels/tests/test_basico_model.py +7 -8
  19. aiagents4pharma/talk2biomodels/tests/test_get_annotation.py +171 -0
  20. aiagents4pharma/talk2biomodels/tests/test_getmodelinfo.py +26 -0
  21. aiagents4pharma/talk2biomodels/tests/test_integration.py +126 -0
  22. aiagents4pharma/talk2biomodels/tests/test_param_scan.py +68 -0
  23. aiagents4pharma/talk2biomodels/tests/test_query_article.py +76 -0
  24. aiagents4pharma/talk2biomodels/tests/test_search_models.py +28 -0
  25. aiagents4pharma/talk2biomodels/tests/test_simulate_model.py +39 -0
  26. aiagents4pharma/talk2biomodels/tests/test_steady_state.py +90 -0
  27. aiagents4pharma/talk2biomodels/tests/test_sys_bio_model.py +13 -7
  28. aiagents4pharma/talk2biomodels/tools/__init__.py +4 -0
  29. aiagents4pharma/talk2biomodels/tools/ask_question.py +59 -25
  30. aiagents4pharma/talk2biomodels/tools/get_annotation.py +304 -0
  31. aiagents4pharma/talk2biomodels/tools/get_modelinfo.py +5 -3
  32. aiagents4pharma/talk2biomodels/tools/load_arguments.py +114 -0
  33. aiagents4pharma/talk2biomodels/tools/parameter_scan.py +287 -0
  34. aiagents4pharma/talk2biomodels/tools/query_article.py +59 -0
  35. aiagents4pharma/talk2biomodels/tools/simulate_model.py +20 -89
  36. aiagents4pharma/talk2biomodels/tools/steady_state.py +167 -0
  37. aiagents4pharma/talk2competitors/__init__.py +5 -0
  38. aiagents4pharma/talk2competitors/agents/__init__.py +6 -0
  39. aiagents4pharma/talk2competitors/agents/main_agent.py +130 -0
  40. aiagents4pharma/talk2competitors/agents/s2_agent.py +75 -0
  41. aiagents4pharma/talk2competitors/config/__init__.py +5 -0
  42. aiagents4pharma/talk2competitors/config/config.py +110 -0
  43. aiagents4pharma/talk2competitors/state/__init__.py +5 -0
  44. aiagents4pharma/talk2competitors/state/state_talk2competitors.py +32 -0
  45. aiagents4pharma/talk2competitors/tests/__init__.py +3 -0
  46. aiagents4pharma/talk2competitors/tests/test_langgraph.py +274 -0
  47. aiagents4pharma/talk2competitors/tools/__init__.py +7 -0
  48. aiagents4pharma/talk2competitors/tools/s2/__init__.py +8 -0
  49. aiagents4pharma/talk2competitors/tools/s2/display_results.py +25 -0
  50. aiagents4pharma/talk2competitors/tools/s2/multi_paper_rec.py +132 -0
  51. aiagents4pharma/talk2competitors/tools/s2/search.py +119 -0
  52. aiagents4pharma/talk2competitors/tools/s2/single_paper_rec.py +141 -0
  53. aiagents4pharma/talk2knowledgegraphs/__init__.py +2 -1
  54. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_enrichments_enrichments.py +39 -0
  55. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_enrichments_ollama.py +117 -0
  56. aiagents4pharma/talk2knowledgegraphs/utils/__init__.py +5 -0
  57. aiagents4pharma/talk2knowledgegraphs/utils/enrichments/__init__.py +5 -0
  58. aiagents4pharma/talk2knowledgegraphs/utils/enrichments/enrichments.py +36 -0
  59. aiagents4pharma/talk2knowledgegraphs/utils/enrichments/ollama.py +123 -0
  60. {aiagents4pharma-1.9.0.dist-info → aiagents4pharma-1.15.0.dist-info}/METADATA +42 -23
  61. aiagents4pharma-1.15.0.dist-info/RECORD +102 -0
  62. aiagents4pharma/talk2biomodels/tests/test_langgraph.py +0 -240
  63. aiagents4pharma-1.9.0.dist-info/RECORD +0 -62
  64. {aiagents4pharma-1.9.0.dist-info → aiagents4pharma-1.15.0.dist-info}/LICENSE +0 -0
  65. {aiagents4pharma-1.9.0.dist-info → aiagents4pharma-1.15.0.dist-info}/WHEEL +0 -0
  66. {aiagents4pharma-1.9.0.dist-info → aiagents4pharma-1.15.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Main agent for the talk2competitors app.
5
+ """
6
+
7
+ import logging
8
+ from typing import Literal
9
+ from dotenv import load_dotenv
10
+ from langchain_core.language_models.chat_models import BaseChatModel
11
+ from langchain_core.messages import AIMessage
12
+ from langchain_openai import ChatOpenAI
13
+ from langgraph.checkpoint.memory import MemorySaver
14
+ from langgraph.graph import END, START, StateGraph
15
+ from langgraph.types import Command
16
+ from ..agents import s2_agent
17
+ from ..config.config import config
18
+ from ..state.state_talk2competitors import Talk2Competitors
19
+
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
+
23
+ load_dotenv()
24
+
25
+ def make_supervisor_node(llm: BaseChatModel) -> str:
26
+ """
27
+ Creates a supervisor node following LangGraph patterns.
28
+
29
+ Args:
30
+ llm (BaseChatModel): The language model to use for generating responses.
31
+
32
+ Returns:
33
+ str: The supervisor node function.
34
+ """
35
+ # options = ["FINISH", "s2_agent"]
36
+
37
+ def supervisor_node(state: Talk2Competitors) -> Command[Literal["s2_agent", "__end__"]]:
38
+ """
39
+ Supervisor node that routes to appropriate sub-agents.
40
+
41
+ Args:
42
+ state (Talk2Competitors): The current state of the conversation.
43
+
44
+ Returns:
45
+ Command[Literal["s2_agent", "__end__"]]: The command to execute next.
46
+ """
47
+ logger.info("Supervisor node called")
48
+
49
+ messages = [{"role": "system", "content": config.MAIN_AGENT_PROMPT}] + state[
50
+ "messages"
51
+ ]
52
+ response = llm.invoke(messages)
53
+ goto = (
54
+ "FINISH"
55
+ if not any(
56
+ kw in state["messages"][-1].content.lower()
57
+ for kw in ["search", "paper", "find"]
58
+ )
59
+ else "s2_agent"
60
+ )
61
+
62
+ if goto == "FINISH":
63
+ return Command(
64
+ goto=END,
65
+ update={
66
+ "messages": state["messages"]
67
+ + [AIMessage(content=response.content)],
68
+ "is_last_step": True,
69
+ "current_agent": None,
70
+ },
71
+ )
72
+
73
+ return Command(
74
+ goto="s2_agent",
75
+ update={
76
+ "messages": state["messages"],
77
+ "is_last_step": False,
78
+ "current_agent": "s2_agent",
79
+ },
80
+ )
81
+
82
+ return supervisor_node
83
+
84
+ def get_app(thread_id: str, llm_model ='gpt-4o-mini') -> StateGraph:
85
+ """
86
+ Returns the langraph app with hierarchical structure.
87
+
88
+ Args:
89
+ thread_id (str): The thread ID for the conversation.
90
+
91
+ Returns:
92
+ The compiled langraph app.
93
+ """
94
+ def call_s2_agent(state: Talk2Competitors) -> Command[Literal["__end__"]]:
95
+ """
96
+ Node for calling the S2 agent.
97
+
98
+ Args:
99
+ state (Talk2Competitors): The current state of the conversation.
100
+
101
+ Returns:
102
+ Command[Literal["__end__"]]: The command to execute next.
103
+ """
104
+ logger.info("Calling S2 agent")
105
+ app = s2_agent.get_app(thread_id, llm_model)
106
+ response = app.invoke(state)
107
+ logger.info("S2 agent completed")
108
+ return Command(
109
+ goto=END,
110
+ update={
111
+ "messages": response["messages"],
112
+ "papers": response.get("papers", []),
113
+ "is_last_step": True,
114
+ "current_agent": "s2_agent",
115
+ },
116
+ )
117
+ llm = ChatOpenAI(model=llm_model, temperature=0)
118
+ workflow = StateGraph(Talk2Competitors)
119
+
120
+ supervisor = make_supervisor_node(llm)
121
+ workflow.add_node("supervisor", supervisor)
122
+ workflow.add_node("s2_agent", call_s2_agent)
123
+
124
+ # Define edges
125
+ workflow.add_edge(START, "supervisor")
126
+ workflow.add_edge("s2_agent", END)
127
+
128
+ app = workflow.compile(checkpointer=MemorySaver())
129
+ logger.info("Main agent workflow compiled")
130
+ return app
@@ -0,0 +1,75 @@
1
+ #/usr/bin/env python3
2
+
3
+ '''
4
+ Agent for interacting with Semantic Scholar
5
+ '''
6
+
7
+ import logging
8
+ from dotenv import load_dotenv
9
+ from langchain_openai import ChatOpenAI
10
+ from langgraph.graph import START, StateGraph
11
+ from langgraph.prebuilt import create_react_agent
12
+ from langgraph.checkpoint.memory import MemorySaver
13
+ from ..config.config import config
14
+ from ..state.state_talk2competitors import Talk2Competitors
15
+ # from ..tools.s2 import s2_tools
16
+ from ..tools.s2.search import search_tool
17
+ from ..tools.s2.display_results import display_results
18
+ from ..tools.s2.single_paper_rec import get_single_paper_recommendations
19
+ from ..tools.s2.multi_paper_rec import get_multi_paper_recommendations
20
+
21
+ load_dotenv()
22
+
23
+ # Initialize logger
24
+ logging.basicConfig(level=logging.INFO)
25
+ logger = logging.getLogger(__name__)
26
+
27
+ def get_app(uniq_id, llm_model='gpt-4o-mini'):
28
+ '''
29
+ This function returns the langraph app.
30
+ '''
31
+ def agent_s2_node(state: Talk2Competitors):
32
+ '''
33
+ This function calls the model.
34
+ '''
35
+ logger.log(logging.INFO, "Creating Agent_S2 node with thread_id %s", uniq_id)
36
+ response = model.invoke(state, {"configurable": {"thread_id": uniq_id}})
37
+ return response
38
+
39
+ # Define the tools
40
+ tools = [search_tool,
41
+ display_results,
42
+ get_single_paper_recommendations,
43
+ get_multi_paper_recommendations]
44
+
45
+ # Create the LLM
46
+ llm = ChatOpenAI(model=llm_model, temperature=0)
47
+ model = create_react_agent(
48
+ llm,
49
+ tools=tools,
50
+ state_schema=Talk2Competitors,
51
+ state_modifier=config.S2_AGENT_PROMPT,
52
+ checkpointer=MemorySaver()
53
+ )
54
+
55
+ # Define a new graph
56
+ workflow = StateGraph(Talk2Competitors)
57
+
58
+ # Define the two nodes we will cycle between
59
+ workflow.add_node("agent_s2", agent_s2_node)
60
+
61
+ # Set the entrypoint as `agent`
62
+ # This means that this node is the first one called
63
+ workflow.add_edge(START, "agent_s2")
64
+
65
+ # Initialize memory to persist state between graph runs
66
+ checkpointer = MemorySaver()
67
+
68
+ # Finally, we compile it!
69
+ # This compiles it into a LangChain Runnable,
70
+ # meaning you can use it as you would any other runnable.
71
+ # Note that we're (optionally) passing the memory when compiling the graph
72
+ app = workflow.compile(checkpointer=checkpointer)
73
+ logger.log(logging.INFO, "Compiled the graph")
74
+
75
+ return app
@@ -0,0 +1,5 @@
1
+ """
2
+ This package contains configuration settings and prompts used by various AI agents
3
+ """
4
+
5
+ from . import config
@@ -0,0 +1,110 @@
1
+ """Configuration module for AI agents handling paper searches and recommendations."""
2
+
3
+
4
+ # pylint: disable=R0903
5
+ class Config:
6
+ """Configuration class containing prompts for AI agents.
7
+
8
+ This class stores prompt templates used by various AI agents in the system,
9
+ particularly for academic paper searches and recommendations.
10
+ """
11
+
12
+ MAIN_AGENT_PROMPT = (
13
+ "You are a supervisory AI agent that routes user queries to specialized tools.\n"
14
+ "Your task is to select the most appropriate tool based on the user's request.\n\n"
15
+ "Available tools and their capabilities:\n\n"
16
+ "1. semantic_scholar_agent:\n"
17
+ " - Search for academic papers and research\n"
18
+ " - Get paper recommendations\n"
19
+ " - Find similar papers\n"
20
+ " USE FOR: Any queries about finding papers, academic research, "
21
+ "or getting paper recommendations\n\n"
22
+ "ROUTING GUIDELINES:\n\n"
23
+ "ALWAYS route to semantic_scholar_agent for:\n"
24
+ "- Finding academic papers\n"
25
+ "- Searching research topics\n"
26
+ "- Getting paper recommendations\n"
27
+ "- Finding similar papers\n"
28
+ "- Any query about academic literature\n\n"
29
+ "Approach:\n"
30
+ "1. Identify the core need in the user's query\n"
31
+ "2. Select the most appropriate tool based on the guidelines above\n"
32
+ "3. If unclear, ask for clarification\n"
33
+ "4. For multi-step tasks, focus on the immediate next step\n\n"
34
+ "Remember:\n"
35
+ "- Be decisive in your tool selection\n"
36
+ "- Focus on the immediate task\n"
37
+ "- Default to semantic_scholar_agent for any paper-finding tasks\n"
38
+ "- Ask for clarification if the request is ambiguous\n\n"
39
+ "When presenting paper search results, always use this exact format:\n\n"
40
+ "Remember to:\n"
41
+ "- Always remember to add the url\n"
42
+ "- Put URLs on the title line itself as markdown\n"
43
+ "- Maintain consistent spacing and formatting"
44
+ )
45
+
46
+ S2_AGENT_PROMPT = (
47
+ "You are a specialized academic research assistant with access to the following tools:\n\n"
48
+ "1. search_papers:\n"
49
+ " USE FOR: General paper searches\n"
50
+ " - Enhances search terms automatically\n"
51
+ " - Adds relevant academic keywords\n"
52
+ " - Focuses on recent research when appropriate\n\n"
53
+ "2. get_single_paper_recommendations:\n"
54
+ " USE FOR: Finding papers similar to a specific paper\n"
55
+ " - Takes a single paper ID\n"
56
+ " - Returns related papers\n\n"
57
+ "3. get_multi_paper_recommendations:\n"
58
+ " USE FOR: Finding papers similar to multiple papers\n"
59
+ " - Takes multiple paper IDs\n"
60
+ " - Finds papers related to all inputs\n\n"
61
+ "GUIDELINES:\n\n"
62
+ "For paper searches:\n"
63
+ "- Enhance search terms with academic language\n"
64
+ "- Include field-specific terminology\n"
65
+ '- Add "recent" or "latest" when appropriate\n'
66
+ "- Keep queries focused and relevant\n\n"
67
+ "For paper recommendations:\n"
68
+ "- Identify paper IDs (40-character hexadecimal strings)\n"
69
+ "- Use single_paper_recommendations for one ID\n"
70
+ "- Use multi_paper_recommendations for multiple IDs\n\n"
71
+ "Best practices:\n"
72
+ "1. Start with a broad search if no paper IDs are provided\n"
73
+ "2. Look for paper IDs in user input\n"
74
+ "3. Enhance search terms for better results\n"
75
+ "4. Consider the academic context\n"
76
+ "5. Be prepared to refine searches based on feedback\n\n"
77
+ "Remember:\n"
78
+ "- Always select the most appropriate tool\n"
79
+ "- Enhance search queries naturally\n"
80
+ "- Consider academic context\n"
81
+ "- Focus on delivering relevant results\n\n"
82
+ "IMPORTANT GUIDELINES FOR PAPER RECOMMENDATIONS:\n\n"
83
+ "For Multiple Papers:\n"
84
+ "- When getting recommendations for multiple papers, always use "
85
+ "get_multi_paper_recommendations tool\n"
86
+ "- DO NOT call get_single_paper_recommendations multiple times\n"
87
+ "- Always pass all paper IDs in a single call to get_multi_paper_recommendations\n"
88
+ '- Use for queries like "find papers related to both/all papers" or '
89
+ '"find similar papers to these papers"\n\n'
90
+ "For Single Paper:\n"
91
+ "- Use get_single_paper_recommendations when focusing on one specific paper\n"
92
+ "- Pass only one paper ID at a time\n"
93
+ '- Use for queries like "find papers similar to this paper" or '
94
+ '"get recommendations for paper X"\n'
95
+ "- Do not use for multiple papers\n\n"
96
+ "Examples:\n"
97
+ '- For "find related papers for both papers":\n'
98
+ " ✓ Use get_multi_paper_recommendations with both paper IDs\n"
99
+ " × Don't make multiple calls to get_single_paper_recommendations\n\n"
100
+ '- For "find papers related to the first paper":\n'
101
+ " ✓ Use get_single_paper_recommendations with just that paper's ID\n"
102
+ " × Don't use get_multi_paper_recommendations\n\n"
103
+ "Remember:\n"
104
+ "- Be precise in identifying which paper ID to use for single recommendations\n"
105
+ "- Don't reuse previous paper IDs unless specifically requested\n"
106
+ "- For fresh paper recommendations, always use the original paper ID"
107
+ )
108
+
109
+
110
+ config = Config()
@@ -0,0 +1,5 @@
1
+ '''
2
+ This file is used to import all the modules in the package.
3
+ '''
4
+
5
+ from . import state_talk2competitors
@@ -0,0 +1,32 @@
1
+ """
2
+ This is the state file for the talk2comp agent.
3
+ """
4
+
5
+ import logging
6
+ from typing import Annotated, Any, Dict, Optional
7
+
8
+ from langgraph.prebuilt.chat_agent_executor import AgentState
9
+ from typing_extensions import NotRequired, Required
10
+
11
+ # Configure logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def replace_dict(existing: Dict[str, Any], new: Dict[str, Any]) -> Dict[str, Any]:
17
+ """Replace the existing dict with the new one."""
18
+ logger.info("Updating existing state %s with the state dict: %s", existing, new)
19
+ return new
20
+
21
+
22
+ class Talk2Competitors(AgentState):
23
+ """
24
+ The state for the talk2comp agent, inheriting from AgentState.
25
+ """
26
+
27
+ papers: Annotated[Dict[str, Any], replace_dict] # Changed from List to Dict
28
+ search_table: NotRequired[str]
29
+ next: str # Required for routing in LangGraph
30
+ current_agent: NotRequired[Optional[str]]
31
+ is_last_step: Required[bool] # Required field for LangGraph
32
+ llm_model: str
@@ -0,0 +1,3 @@
1
+ """
2
+ This module contains the test cases.
3
+ """
@@ -0,0 +1,274 @@
1
+ """
2
+ Unit and integration tests for Talk2Competitors system.
3
+ Each test focuses on a single, specific functionality.
4
+ Tests are deterministic and independent of each other.
5
+ """
6
+
7
+ from unittest.mock import Mock, patch
8
+
9
+ import pytest
10
+ from langchain_core.messages import AIMessage, HumanMessage
11
+
12
+ from ..agents.main_agent import get_app, make_supervisor_node
13
+ from ..state.state_talk2competitors import replace_dict
14
+ from ..tools.s2.display_results import display_results
15
+ from ..tools.s2.multi_paper_rec import get_multi_paper_recommendations
16
+ from ..tools.s2.search import search_tool
17
+ from ..tools.s2.single_paper_rec import get_single_paper_recommendations
18
+
19
+ # pylint: disable=redefined-outer-name
20
+
21
+ # Fixed test data for deterministic results
22
+ MOCK_SEARCH_RESPONSE = {
23
+ "data": [
24
+ {
25
+ "paperId": "123",
26
+ "title": "Machine Learning Basics",
27
+ "abstract": "An introduction to ML",
28
+ "year": 2023,
29
+ "citationCount": 100,
30
+ "url": "https://example.com/paper1",
31
+ "authors": [{"name": "Test Author"}],
32
+ }
33
+ ]
34
+ }
35
+
36
+ MOCK_STATE_PAPER = {
37
+ "123": {
38
+ "Title": "Machine Learning Basics",
39
+ "Abstract": "An introduction to ML",
40
+ "Year": 2023,
41
+ "Citation Count": 100,
42
+ "URL": "https://example.com/paper1",
43
+ }
44
+ }
45
+
46
+
47
+ @pytest.fixture
48
+ def initial_state():
49
+ """Create a base state for tests"""
50
+ return {
51
+ "messages": [],
52
+ "papers": {},
53
+ "is_last_step": False,
54
+ "current_agent": None,
55
+ "llm_model": "gpt-4o-mini",
56
+ }
57
+
58
+
59
+ class TestMainAgent:
60
+ """Unit tests for main agent functionality"""
61
+
62
+ def test_supervisor_routes_search_to_s2(self, initial_state):
63
+ """Verifies that search-related queries are routed to S2 agent"""
64
+ llm_mock = Mock()
65
+ llm_mock.invoke.return_value = AIMessage(content="Search initiated")
66
+
67
+ supervisor = make_supervisor_node(llm_mock)
68
+ state = initial_state.copy()
69
+ state["messages"] = [HumanMessage(content="search for papers")]
70
+
71
+ result = supervisor(state)
72
+ assert result.goto == "s2_agent"
73
+ assert not result.update["is_last_step"]
74
+ assert result.update["current_agent"] == "s2_agent"
75
+
76
+ def test_supervisor_routes_general_to_end(self, initial_state):
77
+ """Verifies that non-search queries end the conversation"""
78
+ llm_mock = Mock()
79
+ llm_mock.invoke.return_value = AIMessage(content="General response")
80
+
81
+ supervisor = make_supervisor_node(llm_mock)
82
+ state = initial_state.copy()
83
+ state["messages"] = [HumanMessage(content="What is ML?")]
84
+
85
+ result = supervisor(state)
86
+ assert result.goto == "__end__"
87
+ assert result.update["is_last_step"]
88
+
89
+
90
+ class TestS2Tools:
91
+ """Unit tests for individual S2 tools"""
92
+
93
+ def test_display_results_shows_papers(self, initial_state):
94
+ """Verifies display_results tool correctly returns papers from state"""
95
+ state = initial_state.copy()
96
+ state["papers"] = MOCK_STATE_PAPER
97
+ result = display_results.invoke(input={"state": state})
98
+ assert result == MOCK_STATE_PAPER
99
+ assert isinstance(result, dict)
100
+
101
+ @patch("requests.get")
102
+ def test_search_finds_papers(self, mock_get):
103
+ """Verifies search tool finds and formats papers correctly"""
104
+ mock_get.return_value.json.return_value = MOCK_SEARCH_RESPONSE
105
+ mock_get.return_value.status_code = 200
106
+
107
+ result = search_tool.invoke(
108
+ input={
109
+ "query": "machine learning",
110
+ "limit": 1,
111
+ "tool_call_id": "test123",
112
+ "id": "test123",
113
+ }
114
+ )
115
+
116
+ assert "papers" in result.update
117
+ assert "messages" in result.update
118
+ papers = result.update["papers"]
119
+ assert isinstance(papers, dict)
120
+ assert len(papers) > 0
121
+ paper = next(iter(papers.values()))
122
+ assert paper["Title"] == "Machine Learning Basics"
123
+ assert paper["Year"] == 2023
124
+
125
+ @patch("requests.get")
126
+ def test_single_paper_rec_basic(self, mock_get):
127
+ """Tests basic single paper recommendation functionality"""
128
+ mock_get.return_value.json.return_value = {
129
+ "recommendedPapers": [MOCK_SEARCH_RESPONSE["data"][0]]
130
+ }
131
+ mock_get.return_value.status_code = 200
132
+
133
+ result = get_single_paper_recommendations.invoke(
134
+ input={
135
+ "paper_id": "123",
136
+ "limit": 1,
137
+ "tool_call_id": "test123",
138
+ "id": "test123",
139
+ }
140
+ )
141
+ assert "papers" in result.update
142
+ assert len(result.update["messages"]) == 1
143
+
144
+ @patch("requests.get")
145
+ def test_single_paper_rec_with_optional_params(self, mock_get):
146
+ """Tests single paper recommendations with year parameter"""
147
+ mock_get.return_value.json.return_value = {
148
+ "recommendedPapers": [MOCK_SEARCH_RESPONSE["data"][0]]
149
+ }
150
+ mock_get.return_value.status_code = 200
151
+
152
+ result = get_single_paper_recommendations.invoke(
153
+ input={
154
+ "paper_id": "123",
155
+ "limit": 1,
156
+ "year": "2023-",
157
+ "tool_call_id": "test123",
158
+ "id": "test123",
159
+ }
160
+ )
161
+ assert "papers" in result.update
162
+
163
+ @patch("requests.post")
164
+ def test_multi_paper_rec_basic(self, mock_post):
165
+ """Tests basic multi-paper recommendation functionality"""
166
+ mock_post.return_value.json.return_value = {
167
+ "recommendedPapers": [MOCK_SEARCH_RESPONSE["data"][0]]
168
+ }
169
+ mock_post.return_value.status_code = 200
170
+
171
+ result = get_multi_paper_recommendations.invoke(
172
+ input={
173
+ "paper_ids": ["123", "456"],
174
+ "limit": 1,
175
+ "tool_call_id": "test123",
176
+ "id": "test123",
177
+ }
178
+ )
179
+ assert "papers" in result.update
180
+ assert len(result.update["messages"]) == 1
181
+
182
+ @patch("requests.post")
183
+ def test_multi_paper_rec_with_optional_params(self, mock_post):
184
+ """Tests multi-paper recommendations with all optional parameters"""
185
+ mock_post.return_value.json.return_value = {
186
+ "recommendedPapers": [MOCK_SEARCH_RESPONSE["data"][0]]
187
+ }
188
+ mock_post.return_value.status_code = 200
189
+
190
+ result = get_multi_paper_recommendations.invoke(
191
+ input={
192
+ "paper_ids": ["123", "456"],
193
+ "limit": 1,
194
+ "year": "2023-",
195
+ "tool_call_id": "test123",
196
+ "id": "test123",
197
+ }
198
+ )
199
+ assert "papers" in result.update
200
+ assert len(result.update["messages"]) == 1
201
+
202
+ @patch("requests.get")
203
+ def test_single_paper_rec_empty_response(self, mock_get):
204
+ """Tests single paper recommendations with empty response"""
205
+ mock_get.return_value.json.return_value = {"recommendedPapers": []}
206
+ mock_get.return_value.status_code = 200
207
+
208
+ result = get_single_paper_recommendations.invoke(
209
+ input={
210
+ "paper_id": "123",
211
+ "limit": 1,
212
+ "tool_call_id": "test123",
213
+ "id": "test123",
214
+ }
215
+ )
216
+ assert "papers" in result.update
217
+ assert len(result.update["papers"]) == 0
218
+
219
+ @patch("requests.post")
220
+ def test_multi_paper_rec_empty_response(self, mock_post):
221
+ """Tests multi-paper recommendations with empty response"""
222
+ mock_post.return_value.json.return_value = {"recommendedPapers": []}
223
+ mock_post.return_value.status_code = 200
224
+
225
+ result = get_multi_paper_recommendations.invoke(
226
+ input={
227
+ "paper_ids": ["123", "456"],
228
+ "limit": 1,
229
+ "tool_call_id": "test123",
230
+ "id": "test123",
231
+ }
232
+ )
233
+ assert "papers" in result.update
234
+ assert len(result.update["papers"]) == 0
235
+
236
+
237
+ def test_state_replace_dict():
238
+ """Verifies state dictionary replacement works correctly"""
239
+ existing = {"key1": "value1", "key2": "value2"}
240
+ new = {"key3": "value3"}
241
+ result = replace_dict(existing, new)
242
+ assert result == new
243
+ assert isinstance(result, dict)
244
+
245
+
246
+ @pytest.mark.integration
247
+ def test_end_to_end_search_workflow(initial_state):
248
+ """Integration test: Complete search workflow"""
249
+ with (
250
+ patch("requests.get") as mock_get,
251
+ patch("langchain_openai.ChatOpenAI") as mock_llm,
252
+ ):
253
+ mock_get.return_value.json.return_value = MOCK_SEARCH_RESPONSE
254
+ mock_get.return_value.status_code = 200
255
+
256
+ llm_instance = Mock()
257
+ llm_instance.invoke.return_value = AIMessage(content="Search completed")
258
+ mock_llm.return_value = llm_instance
259
+
260
+ app = get_app("test_integration")
261
+ test_state = initial_state.copy()
262
+ test_state["messages"] = [HumanMessage(content="search for ML papers")]
263
+
264
+ config = {
265
+ "configurable": {
266
+ "thread_id": "test_integration",
267
+ "checkpoint_ns": "test",
268
+ "checkpoint_id": "test123",
269
+ }
270
+ }
271
+
272
+ response = app.invoke(test_state, config)
273
+ assert "papers" in response
274
+ assert len(response["messages"]) > 0
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+
3
+ '''
4
+ Import statements
5
+ '''
6
+
7
+ from . import s2
@@ -0,0 +1,8 @@
1
+ '''
2
+ This file is used to import all the modules in the package.
3
+ '''
4
+
5
+ from . import display_results
6
+ from . import multi_paper_rec
7
+ from . import search
8
+ from . import single_paper_rec
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+
3
+ '''
4
+ This tool is used to display the table of studies.
5
+ '''
6
+
7
+ import logging
8
+ from typing import Annotated
9
+ from langchain_core.tools import tool
10
+ from langgraph.prebuilt import InjectedState
11
+
12
+ # Configure logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+ @tool('display_results')
17
+ def display_results(state: Annotated[dict, InjectedState]):
18
+ """
19
+ Display the papers in the state.
20
+
21
+ Args:
22
+ state (dict): The state of the agent.
23
+ """
24
+ logger.info("Displaying papers from the state")
25
+ return state["papers"]