aiagents4pharma 1.9.0__py3-none-any.whl → 1.10.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.
- aiagents4pharma/__init__.py +9 -6
- aiagents4pharma/talk2competitors/__init__.py +5 -0
- aiagents4pharma/talk2competitors/agents/__init__.py +6 -0
- aiagents4pharma/talk2competitors/agents/main_agent.py +130 -0
- aiagents4pharma/talk2competitors/agents/s2_agent.py +75 -0
- aiagents4pharma/talk2competitors/config/__init__.py +5 -0
- aiagents4pharma/talk2competitors/config/config.py +110 -0
- aiagents4pharma/talk2competitors/state/__init__.py +5 -0
- aiagents4pharma/talk2competitors/state/state_talk2competitors.py +32 -0
- aiagents4pharma/talk2competitors/tests/__init__.py +3 -0
- aiagents4pharma/talk2competitors/tests/test_langgraph.py +274 -0
- aiagents4pharma/talk2competitors/tools/__init__.py +7 -0
- aiagents4pharma/talk2competitors/tools/s2/__init__.py +8 -0
- aiagents4pharma/talk2competitors/tools/s2/display_results.py +25 -0
- aiagents4pharma/talk2competitors/tools/s2/multi_paper_rec.py +132 -0
- aiagents4pharma/talk2competitors/tools/s2/search.py +119 -0
- aiagents4pharma/talk2competitors/tools/s2/single_paper_rec.py +141 -0
- {aiagents4pharma-1.9.0.dist-info → aiagents4pharma-1.10.0.dist-info}/METADATA +37 -18
- {aiagents4pharma-1.9.0.dist-info → aiagents4pharma-1.10.0.dist-info}/RECORD +22 -7
- {aiagents4pharma-1.9.0.dist-info → aiagents4pharma-1.10.0.dist-info}/LICENSE +0 -0
- {aiagents4pharma-1.9.0.dist-info → aiagents4pharma-1.10.0.dist-info}/WHEEL +0 -0
- {aiagents4pharma-1.9.0.dist-info → aiagents4pharma-1.10.0.dist-info}/top_level.txt +0 -0
aiagents4pharma/__init__.py
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
-
|
1
|
+
"""
|
2
2
|
This file is used to import aiagents4pharma modules.
|
3
|
-
|
3
|
+
"""
|
4
4
|
|
5
|
-
from . import
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
from . import (
|
6
|
+
configs,
|
7
|
+
talk2biomodels,
|
8
|
+
talk2cells,
|
9
|
+
talk2competitors,
|
10
|
+
talk2knowledgegraphs,
|
11
|
+
)
|
@@ -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,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,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,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,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"]
|
@@ -0,0 +1,132 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
"""
|
4
|
+
multi_paper_rec: Tool for getting recommendations
|
5
|
+
based on multiple papers
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import logging
|
10
|
+
from typing import Annotated, Any, Dict, List, Optional
|
11
|
+
|
12
|
+
import pandas as pd
|
13
|
+
import requests
|
14
|
+
from langchain_core.messages import ToolMessage
|
15
|
+
from langchain_core.tools import tool
|
16
|
+
from langchain_core.tools.base import InjectedToolCallId
|
17
|
+
from langgraph.types import Command
|
18
|
+
from pydantic import BaseModel, Field
|
19
|
+
|
20
|
+
|
21
|
+
class MultiPaperRecInput(BaseModel):
|
22
|
+
"""Input schema for multiple paper recommendations tool."""
|
23
|
+
|
24
|
+
paper_ids: List[str] = Field(
|
25
|
+
description=("List of Semantic Scholar Paper IDs to get recommendations for")
|
26
|
+
)
|
27
|
+
limit: int = Field(
|
28
|
+
default=2,
|
29
|
+
description="Maximum total number of recommendations to return",
|
30
|
+
ge=1,
|
31
|
+
le=500,
|
32
|
+
)
|
33
|
+
year: Optional[str] = Field(
|
34
|
+
default=None,
|
35
|
+
description="Year range in format: YYYY for specific year, "
|
36
|
+
"YYYY- for papers after year, -YYYY for papers before year, or YYYY:YYYY for range",
|
37
|
+
)
|
38
|
+
tool_call_id: Annotated[str, InjectedToolCallId]
|
39
|
+
|
40
|
+
model_config = {"arbitrary_types_allowed": True}
|
41
|
+
|
42
|
+
|
43
|
+
@tool(args_schema=MultiPaperRecInput)
|
44
|
+
def get_multi_paper_recommendations(
|
45
|
+
paper_ids: List[str],
|
46
|
+
tool_call_id: Annotated[str, InjectedToolCallId],
|
47
|
+
limit: int = 2,
|
48
|
+
year: Optional[str] = None,
|
49
|
+
) -> Dict[str, Any]:
|
50
|
+
"""
|
51
|
+
Get paper recommendations based on multiple papers.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
paper_ids (List[str]): The list of paper IDs to base recommendations on.
|
55
|
+
tool_call_id (Annotated[str, InjectedToolCallId]): The tool call ID.
|
56
|
+
limit (int, optional): The maximum number of recommendations to return. Defaults to 2.
|
57
|
+
year (str, optional): Year range for papers.
|
58
|
+
Supports formats like "2024-", "-2024", "2024:2025". Defaults to None.
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
Dict[str, Any]: The recommendations and related information.
|
62
|
+
"""
|
63
|
+
logging.info("Starting multi-paper recommendations search.")
|
64
|
+
|
65
|
+
endpoint = "https://api.semanticscholar.org/recommendations/v1/papers"
|
66
|
+
headers = {"Content-Type": "application/json"}
|
67
|
+
payload = {"positivePaperIds": paper_ids, "negativePaperIds": []}
|
68
|
+
params = {
|
69
|
+
"limit": min(limit, 500),
|
70
|
+
"fields": "paperId,title,abstract,year,authors,citationCount,url",
|
71
|
+
}
|
72
|
+
|
73
|
+
# Add year parameter if provided
|
74
|
+
if year:
|
75
|
+
params["year"] = year
|
76
|
+
|
77
|
+
# Getting recommendations
|
78
|
+
response = requests.post(
|
79
|
+
endpoint,
|
80
|
+
headers=headers,
|
81
|
+
params=params,
|
82
|
+
data=json.dumps(payload),
|
83
|
+
timeout=10,
|
84
|
+
)
|
85
|
+
logging.info(
|
86
|
+
"API Response Status for multi-paper recommendations: %s", response.status_code
|
87
|
+
)
|
88
|
+
|
89
|
+
data = response.json()
|
90
|
+
recommendations = data.get("recommendedPapers", [])
|
91
|
+
|
92
|
+
# Create a dictionary to store the papers
|
93
|
+
filtered_papers = {
|
94
|
+
paper["paperId"]: {
|
95
|
+
"Title": paper.get("title", "N/A"),
|
96
|
+
"Abstract": paper.get("abstract", "N/A"),
|
97
|
+
"Year": paper.get("year", "N/A"),
|
98
|
+
"Citation Count": paper.get("citationCount", "N/A"),
|
99
|
+
"URL": paper.get("url", "N/A"),
|
100
|
+
}
|
101
|
+
for paper in recommendations
|
102
|
+
if paper.get("title") and paper.get("paperId")
|
103
|
+
}
|
104
|
+
|
105
|
+
# Create a DataFrame from the dictionary
|
106
|
+
df = pd.DataFrame.from_dict(filtered_papers, orient="index")
|
107
|
+
# print("Created DataFrame with results:")
|
108
|
+
logging.info("Created DataFrame with results: %s", df)
|
109
|
+
|
110
|
+
# Format papers for state update
|
111
|
+
papers = [
|
112
|
+
f"Paper ID: {paper_id}\n"
|
113
|
+
f"Title: {paper_data['Title']}\n"
|
114
|
+
f"Abstract: {paper_data['Abstract']}\n"
|
115
|
+
f"Year: {paper_data['Year']}\n"
|
116
|
+
f"Citations: {paper_data['Citation Count']}\n"
|
117
|
+
f"URL: {paper_data['URL']}\n"
|
118
|
+
for paper_id, paper_data in filtered_papers.items()
|
119
|
+
]
|
120
|
+
|
121
|
+
# Convert DataFrame to markdown table
|
122
|
+
markdown_table = df.to_markdown(tablefmt="grid")
|
123
|
+
logging.info("Search results: %s", papers)
|
124
|
+
|
125
|
+
return Command(
|
126
|
+
update={
|
127
|
+
"papers": filtered_papers, # Now sending the dictionary directly
|
128
|
+
"messages": [
|
129
|
+
ToolMessage(content=markdown_table, tool_call_id=tool_call_id)
|
130
|
+
],
|
131
|
+
}
|
132
|
+
)
|
@@ -0,0 +1,119 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
"""
|
4
|
+
This tool is used to search for academic papers on Semantic Scholar.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from typing import Annotated, Any, Dict, Optional
|
9
|
+
|
10
|
+
import pandas as pd
|
11
|
+
import requests
|
12
|
+
from langchain_core.messages import ToolMessage
|
13
|
+
from langchain_core.tools import tool
|
14
|
+
from langchain_core.tools.base import InjectedToolCallId
|
15
|
+
from langgraph.types import Command
|
16
|
+
from pydantic import BaseModel, Field
|
17
|
+
|
18
|
+
|
19
|
+
class SearchInput(BaseModel):
|
20
|
+
"""Input schema for the search papers tool."""
|
21
|
+
|
22
|
+
query: str = Field(
|
23
|
+
description="Search query string to find academic papers."
|
24
|
+
"Be specific and include relevant academic terms."
|
25
|
+
)
|
26
|
+
limit: int = Field(
|
27
|
+
default=2, description="Maximum number of results to return", ge=1, le=100
|
28
|
+
)
|
29
|
+
year: Optional[str] = Field(
|
30
|
+
default=None,
|
31
|
+
description="Year range in format: YYYY for specific year, "
|
32
|
+
"YYYY- for papers after year, -YYYY for papers before year, or YYYY:YYYY for range",
|
33
|
+
)
|
34
|
+
tool_call_id: Annotated[str, InjectedToolCallId]
|
35
|
+
|
36
|
+
|
37
|
+
@tool(args_schema=SearchInput)
|
38
|
+
def search_tool(
|
39
|
+
query: str,
|
40
|
+
tool_call_id: Annotated[str, InjectedToolCallId],
|
41
|
+
limit: int = 2,
|
42
|
+
year: Optional[str] = None,
|
43
|
+
) -> Dict[str, Any]:
|
44
|
+
"""
|
45
|
+
Search for academic papers on Semantic Scholar.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
query (str): The search query string to find academic papers.
|
49
|
+
tool_call_id (Annotated[str, InjectedToolCallId]): The tool call ID.
|
50
|
+
limit (int, optional): The maximum number of results to return. Defaults to 2.
|
51
|
+
year (str, optional): Year range for papers.
|
52
|
+
Supports formats like "2024-", "-2024", "2024:2025". Defaults to None.
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
Dict[str, Any]: The search results and related information.
|
56
|
+
"""
|
57
|
+
print("Starting paper search...")
|
58
|
+
endpoint = "https://api.semanticscholar.org/graph/v1/paper/search"
|
59
|
+
params = {
|
60
|
+
"query": query,
|
61
|
+
"limit": min(limit, 100),
|
62
|
+
# "fields": "paperId,title,abstract,year,authors,
|
63
|
+
# citationCount,url,publicationTypes,openAccessPdf",
|
64
|
+
"fields": "paperId,title,abstract,year,authors,citationCount,url",
|
65
|
+
}
|
66
|
+
|
67
|
+
# Add year parameter if provided
|
68
|
+
if year:
|
69
|
+
params["year"] = year
|
70
|
+
|
71
|
+
response = requests.get(endpoint, params=params, timeout=10)
|
72
|
+
data = response.json()
|
73
|
+
papers = data.get("data", [])
|
74
|
+
|
75
|
+
# Create a dictionary to store the papers
|
76
|
+
filtered_papers = {
|
77
|
+
paper["paperId"]: {
|
78
|
+
"Title": paper.get("title", "N/A"),
|
79
|
+
"Abstract": paper.get("abstract", "N/A"),
|
80
|
+
"Year": paper.get("year", "N/A"),
|
81
|
+
"Citation Count": paper.get("citationCount", "N/A"),
|
82
|
+
"URL": paper.get("url", "N/A"),
|
83
|
+
# "Publication Type": paper.get("publicationTypes", ["N/A"])[0]
|
84
|
+
# if paper.get("publicationTypes")
|
85
|
+
# else "N/A",
|
86
|
+
# "Open Access PDF": paper.get("openAccessPdf", {}).get("url", "N/A")
|
87
|
+
# if paper.get("openAccessPdf") is not None
|
88
|
+
# else "N/A",
|
89
|
+
}
|
90
|
+
for paper in papers
|
91
|
+
if paper.get("title") and paper.get("authors")
|
92
|
+
}
|
93
|
+
|
94
|
+
df = pd.DataFrame(filtered_papers)
|
95
|
+
|
96
|
+
# Format papers for state update
|
97
|
+
papers = [
|
98
|
+
f"Paper ID: {paper_id}\n"
|
99
|
+
f"Title: {paper_data['Title']}\n"
|
100
|
+
f"Abstract: {paper_data['Abstract']}\n"
|
101
|
+
f"Year: {paper_data['Year']}\n"
|
102
|
+
f"Citations: {paper_data['Citation Count']}\n"
|
103
|
+
f"URL: {paper_data['URL']}\n"
|
104
|
+
# f"Publication Type: {paper_data['Publication Type']}\n"
|
105
|
+
# f"Open Access PDF: {paper_data['Open Access PDF']}"
|
106
|
+
for paper_id, paper_data in filtered_papers.items()
|
107
|
+
]
|
108
|
+
|
109
|
+
markdown_table = df.to_markdown(tablefmt="grid")
|
110
|
+
logging.info("Search results: %s", papers)
|
111
|
+
|
112
|
+
return Command(
|
113
|
+
update={
|
114
|
+
"papers": filtered_papers, # Now sending the dictionary directly
|
115
|
+
"messages": [
|
116
|
+
ToolMessage(content=markdown_table, tool_call_id=tool_call_id)
|
117
|
+
],
|
118
|
+
}
|
119
|
+
)
|
@@ -0,0 +1,141 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
"""
|
4
|
+
This tool is used to return recommendations for a single paper.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from typing import Annotated, Any, Dict, Optional
|
9
|
+
|
10
|
+
import pandas as pd
|
11
|
+
import requests
|
12
|
+
from langchain_core.messages import ToolMessage
|
13
|
+
from langchain_core.tools import tool
|
14
|
+
from langchain_core.tools.base import InjectedToolCallId
|
15
|
+
from langgraph.types import Command
|
16
|
+
from pydantic import BaseModel, Field
|
17
|
+
|
18
|
+
# Configure logging
|
19
|
+
logging.basicConfig(level=logging.INFO)
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class SinglePaperRecInput(BaseModel):
|
24
|
+
"""Input schema for single paper recommendation tool."""
|
25
|
+
|
26
|
+
paper_id: str = Field(
|
27
|
+
description="Semantic Scholar Paper ID to get recommendations for (40-character string)"
|
28
|
+
)
|
29
|
+
limit: int = Field(
|
30
|
+
default=2,
|
31
|
+
description="Maximum number of recommendations to return",
|
32
|
+
ge=1,
|
33
|
+
le=500,
|
34
|
+
)
|
35
|
+
year: Optional[str] = Field(
|
36
|
+
default=None,
|
37
|
+
description="Year range in format: YYYY for specific year, "
|
38
|
+
"YYYY- for papers after year, -YYYY for papers before year, or YYYY:YYYY for range",
|
39
|
+
)
|
40
|
+
tool_call_id: Annotated[str, InjectedToolCallId]
|
41
|
+
model_config = {"arbitrary_types_allowed": True}
|
42
|
+
|
43
|
+
|
44
|
+
@tool(args_schema=SinglePaperRecInput)
|
45
|
+
def get_single_paper_recommendations(
|
46
|
+
paper_id: str,
|
47
|
+
tool_call_id: Annotated[str, InjectedToolCallId],
|
48
|
+
limit: int = 2,
|
49
|
+
year: Optional[str] = None,
|
50
|
+
) -> Dict[str, Any]:
|
51
|
+
"""
|
52
|
+
Get paper recommendations based on a single paper.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
paper_id (str): The Semantic Scholar Paper ID to get recommendations for.
|
56
|
+
tool_call_id (Annotated[str, InjectedToolCallId]): The tool call ID.
|
57
|
+
limit (int, optional): The maximum number of recommendations to return. Defaults to 2.
|
58
|
+
year (str, optional): Year range for papers.
|
59
|
+
Supports formats like "2024-", "-2024", "2024:2025". Defaults to None.
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
Dict[str, Any]: The recommendations and related information.
|
63
|
+
"""
|
64
|
+
logger.info("Starting single paper recommendations search.")
|
65
|
+
|
66
|
+
endpoint = (
|
67
|
+
f"https://api.semanticscholar.org/recommendations/v1/papers/forpaper/{paper_id}"
|
68
|
+
)
|
69
|
+
params = {
|
70
|
+
"limit": min(limit, 500), # Max 500 per API docs
|
71
|
+
"fields": "paperId,title,abstract,year,authors,citationCount,url",
|
72
|
+
"from": "all-cs", # Using all-cs pool as specified in docs
|
73
|
+
}
|
74
|
+
|
75
|
+
# Add year parameter if provided
|
76
|
+
if year:
|
77
|
+
params["year"] = year
|
78
|
+
|
79
|
+
response = requests.get(endpoint, params=params, timeout=10)
|
80
|
+
data = response.json()
|
81
|
+
papers = data.get("data", [])
|
82
|
+
response = requests.get(endpoint, params=params, timeout=10)
|
83
|
+
# print(f"API Response Status: {response.status_code}")
|
84
|
+
logging.info(
|
85
|
+
"API Response Status for recommendations of paper %s: %s",
|
86
|
+
paper_id,
|
87
|
+
response.status_code,
|
88
|
+
)
|
89
|
+
# print(f"Request params: {params}")
|
90
|
+
logging.info("Request params: %s", params)
|
91
|
+
|
92
|
+
data = response.json()
|
93
|
+
recommendations = data.get("recommendedPapers", [])
|
94
|
+
|
95
|
+
# Extract paper ID and title from recommendations
|
96
|
+
filtered_papers = {
|
97
|
+
paper["paperId"]: {
|
98
|
+
"Title": paper.get("title", "N/A"),
|
99
|
+
"Abstract": paper.get("abstract", "N/A"),
|
100
|
+
"Year": paper.get("year", "N/A"),
|
101
|
+
"Citation Count": paper.get("citationCount", "N/A"),
|
102
|
+
"URL": paper.get("url", "N/A"),
|
103
|
+
# "Publication Type": paper.get("publicationTypes", ["N/A"])[0]
|
104
|
+
# if paper.get("publicationTypes")
|
105
|
+
# else "N/A",
|
106
|
+
# "Open Access PDF": paper.get("openAccessPdf", {}).get("url", "N/A")
|
107
|
+
# if paper.get("openAccessPdf") is not None
|
108
|
+
# else "N/A",
|
109
|
+
}
|
110
|
+
for paper in recommendations
|
111
|
+
if paper.get("title") and paper.get("authors")
|
112
|
+
}
|
113
|
+
|
114
|
+
# Create a DataFrame for pretty printing
|
115
|
+
df = pd.DataFrame(filtered_papers)
|
116
|
+
|
117
|
+
# Format papers for state update
|
118
|
+
papers = [
|
119
|
+
f"Paper ID: {paper_id}\n"
|
120
|
+
f"Title: {paper_data['Title']}\n"
|
121
|
+
f"Abstract: {paper_data['Abstract']}\n"
|
122
|
+
f"Year: {paper_data['Year']}\n"
|
123
|
+
f"Citations: {paper_data['Citation Count']}\n"
|
124
|
+
f"URL: {paper_data['URL']}\n"
|
125
|
+
# f"Publication Type: {paper_data['Publication Type']}\n"
|
126
|
+
# f"Open Access PDF: {paper_data['Open Access PDF']}"
|
127
|
+
for paper_id, paper_data in filtered_papers.items()
|
128
|
+
]
|
129
|
+
|
130
|
+
# Convert DataFrame to markdown table
|
131
|
+
markdown_table = df.to_markdown(tablefmt="grid")
|
132
|
+
logging.info("Search results: %s", papers)
|
133
|
+
|
134
|
+
return Command(
|
135
|
+
update={
|
136
|
+
"papers": filtered_papers, # Now sending the dictionary directly
|
137
|
+
"messages": [
|
138
|
+
ToolMessage(content=markdown_table, tool_call_id=tool_call_id)
|
139
|
+
],
|
140
|
+
}
|
141
|
+
)
|
@@ -1,11 +1,11 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: aiagents4pharma
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.10.0
|
4
4
|
Summary: AI Agents for drug discovery, drug development, and other pharmaceutical R&D
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
7
7
|
Classifier: Operating System :: OS Independent
|
8
|
-
Requires-Python: >=3.
|
8
|
+
Requires-Python: >=3.12
|
9
9
|
Description-Content-Type: text/markdown
|
10
10
|
License-File: LICENSE
|
11
11
|
Requires-Dist: copasi_basico==0.78
|
@@ -48,6 +48,9 @@ Requires-Dist: streamlit-feedback
|
|
48
48
|
[](https://github.com/VirtualPatientEngine/AIAgents4Pharma/actions/workflows/tests_talk2cells.yml)
|
49
49
|
[](https://github.com/VirtualPatientEngine/AIAgents4Pharma/actions/workflows/tests_talk2knowledgegraphs.yml)
|
50
50
|
[](https://github.com/VirtualPatientEngine/AIAgents4Pharma/actions/workflows/tests_talk2competitors.yml)
|
51
|
+

|
52
|
+

|
53
|
+
|
51
54
|
|
52
55
|
<h1 align="center" style="border-bottom: none;">🤖 AIAgents4Pharma</h1>
|
53
56
|
|
@@ -56,9 +59,9 @@ Welcome to **AIAgents4Pharma** – an open-source project by [Team VPE](https://
|
|
56
59
|
Our toolkit currently consists of three intelligent agents, each designed to simplify and enhance access to specialized data in biology:
|
57
60
|
|
58
61
|
- **Talk2BioModels**: Engage directly with mathematical models in systems biology.
|
59
|
-
- **Talk2Cells**
|
60
|
-
- **Talk2KnowledgeGraphs**
|
61
|
-
- **Talk2Competitors**
|
62
|
+
- **Talk2Cells** _(Work in progress)_: Query and analyze sequencing data with ease.
|
63
|
+
- **Talk2KnowledgeGraphs** _(Work in progress)_: Access and explore complex biological knowledge graphs for insightful data connections.
|
64
|
+
- **Talk2Competitors** _(Coming soon)_: Get recommendations for articles related to your choice. Download, query, and write/retrieve them to your reference manager (currently supporting Zotero).
|
62
65
|
|
63
66
|
---
|
64
67
|
|
@@ -72,15 +75,15 @@ Our toolkit currently consists of three intelligent agents, each designed to sim
|
|
72
75
|
- Adjust parameters within the model to simulate different conditions.
|
73
76
|
- Query simulation results.
|
74
77
|
|
75
|
-
### 2. Talk2Cells
|
78
|
+
### 2. Talk2Cells _(Work in Progress)_
|
76
79
|
|
77
80
|
**Talk2Cells** is being developed to provide direct access to and analysis of sequencing data, such as RNA-Seq or DNA-Seq, using natural language.
|
78
81
|
|
79
|
-
### 3. Talk2KnowledgeGraphs
|
82
|
+
### 3. Talk2KnowledgeGraphs _(Work in Progress)_
|
80
83
|
|
81
84
|
**Talk2KnowledgeGraphs** is an agent designed to enable interaction with biological knowledge graphs (KGs). KGs integrate vast amounts of structured biological data into a format that highlights relationships between entities, such as proteins, genes, and diseases.
|
82
85
|
|
83
|
-
### 4.
|
86
|
+
### 4. Talk2Competitors _(Coming soon)_
|
84
87
|
|
85
88
|
## Getting Started
|
86
89
|
|
@@ -91,48 +94,60 @@ Our toolkit currently consists of three intelligent agents, each designed to sim
|
|
91
94
|
- Required libraries specified in `requirements.txt`
|
92
95
|
|
93
96
|
### Installation
|
97
|
+
|
94
98
|
#### Option 1: PyPI
|
95
|
-
|
96
|
-
|
97
|
-
|
99
|
+
|
100
|
+
```bash
|
101
|
+
pip install aiagents4pharma
|
102
|
+
```
|
98
103
|
|
99
104
|
Check out the tutorials on each agent for detailed instrcutions.
|
100
105
|
|
101
106
|
#### Option 2: git
|
107
|
+
|
102
108
|
1. **Clone the repository:**
|
109
|
+
|
103
110
|
```bash
|
104
111
|
git clone https://github.com/VirtualPatientEngine/AIAgents4Pharma
|
105
112
|
cd AIAgents4Pharma
|
106
113
|
```
|
107
114
|
|
108
115
|
2. **Install dependencies:**
|
116
|
+
|
109
117
|
```bash
|
110
118
|
pip install .
|
111
119
|
```
|
112
120
|
|
113
121
|
3. **Initialize OPENAI_API_KEY**
|
122
|
+
|
114
123
|
```bash
|
115
|
-
export OPENAI_API_KEY
|
124
|
+
export OPENAI_API_KEY=....
|
116
125
|
```
|
117
126
|
|
118
127
|
4. **[Optional] Set up login credentials**
|
128
|
+
|
119
129
|
```bash
|
120
130
|
vi .streamlit/secrets.toml
|
121
131
|
```
|
132
|
+
|
122
133
|
and enter
|
134
|
+
|
123
135
|
```
|
124
136
|
password='XXX'
|
125
137
|
```
|
126
|
-
|
138
|
+
|
139
|
+
Please note that the passoword will be same for all the users.
|
127
140
|
|
128
141
|
5. **[Optional] Initialize LANGSMITH_API_KEY**
|
142
|
+
|
129
143
|
```bash
|
130
144
|
export LANGCHAIN_TRACING_V2=true
|
131
145
|
export LANGCHAIN_API_KEY=<your-api-key>
|
132
146
|
```
|
133
|
-
|
134
|
-
|
135
|
-
|
147
|
+
|
148
|
+
Please note that this will create a new tracing project in your Langsmith
|
149
|
+
account with the name `<user_name>@<uuid>`, where `user_name` is the name
|
150
|
+
you provided in the previous step. If you skip the previous step, it will
|
136
151
|
default to `default`. <uuid> will be the 128 bit unique ID created for the
|
137
152
|
session.
|
138
153
|
|
@@ -164,6 +179,7 @@ We welcome contributions to AIAgents4Pharma! Here’s how you can help:
|
|
164
179
|
5. **Open a pull request**
|
165
180
|
|
166
181
|
### Current Needs
|
182
|
+
|
167
183
|
- **Beta testers** for Talk2BioModels.
|
168
184
|
- **Developers** with experience in natural language processing, bioinformatics, or knowledge graphs for contributions to AIAgents4Pharma.
|
169
185
|
|
@@ -174,19 +190,22 @@ Check out our [CONTRIBUTING.md](CONTRIBUTING.md) for more information.
|
|
174
190
|
## Roadmap
|
175
191
|
|
176
192
|
### Completed
|
193
|
+
|
177
194
|
- **Talk2BioModels**: Initial release with core capabilities for interacting with systems biology models.
|
178
195
|
|
179
196
|
### Planned
|
197
|
+
|
180
198
|
- **User Interface**: Interactive web UI for all agents.
|
181
199
|
- **Talk2Cells**: Integration of sequencing data analysis tools.
|
182
200
|
- **Talk2KnowledgeGraphs**: Interface for biological knowledge graph interaction.
|
183
|
-
- **Talk2Competitors
|
201
|
+
- **Talk2Competitors**: Interface for exploring articles
|
184
202
|
|
185
|
-
We’re excited to bring AIAgents4Pharma to the bioinformatics and pharmaceutical research community. Together, let’s make data-driven biological research more accessible and insightful.
|
203
|
+
We’re excited to bring AIAgents4Pharma to the bioinformatics and pharmaceutical research community. Together, let’s make data-driven biological research more accessible and insightful.
|
186
204
|
|
187
205
|
**Get Started** with AIAgents4Pharma today and transform the way you interact with biological data.
|
188
206
|
|
189
207
|
---
|
190
208
|
|
191
209
|
## Feedback
|
210
|
+
|
192
211
|
Questions/Bug reports/Feature requests/Comments/Suggestions? We welcome all. Please use the `Isssues` tab 😀
|
@@ -1,4 +1,4 @@
|
|
1
|
-
aiagents4pharma/__init__.py,sha256=
|
1
|
+
aiagents4pharma/__init__.py,sha256=5muWWIg89VHPybfxonO_5xOMJPasKNsGdQRhozDaEmk,177
|
2
2
|
aiagents4pharma/configs/__init__.py,sha256=hNkSrXw1Ix1HhkGn_aaidr2coBYySfM0Hm_pMeRcX7k,76
|
3
3
|
aiagents4pharma/configs/config.yaml,sha256=8y8uG6Dzx4-9jyb6hZ8r4lOJz5gA_sQhCiSCgXL5l7k,65
|
4
4
|
aiagents4pharma/configs/talk2biomodels/__init__.py,sha256=5ah__-8XyRblwT0U1ByRigNjt_GyCheu7zce4aM-eZE,68
|
@@ -34,7 +34,22 @@ aiagents4pharma/talk2cells/tools/__init__.py,sha256=38nK2a_lEFRjO3qD6Fo9a3983ZCY
|
|
34
34
|
aiagents4pharma/talk2cells/tools/scp_agent/__init__.py,sha256=s7g0lyH1lMD9pcWHLPtwRJRvzmTh2II7DrxyLulpjmQ,163
|
35
35
|
aiagents4pharma/talk2cells/tools/scp_agent/display_studies.py,sha256=6q59gh_NQaiOU2rn55A3sIIFKlXi4SK3iKgySvUDrtQ,600
|
36
36
|
aiagents4pharma/talk2cells/tools/scp_agent/search_studies.py,sha256=MLe-twtFnOu-P8P9diYq7jvHBHbWFRRCZLcfpUzqPMg,2806
|
37
|
-
aiagents4pharma/talk2competitors/__init__.py,sha256=
|
37
|
+
aiagents4pharma/talk2competitors/__init__.py,sha256=haaikzND3c0Euqq86ndA4fl9q42aOop5rYG_8Zh1D-o,119
|
38
|
+
aiagents4pharma/talk2competitors/agents/__init__.py,sha256=ykszlVGxz3egLHZAttlNoTPxIrnQJZYva_ssR8fwIFk,117
|
39
|
+
aiagents4pharma/talk2competitors/agents/main_agent.py,sha256=UoHCpZd-HoeG0B6_gAF1cEP2OqMvrTuGe7MZDwL_u1U,3878
|
40
|
+
aiagents4pharma/talk2competitors/agents/s2_agent.py,sha256=eTrhc4ZPvWOUWMHNYxK0WltsZedZUnAWNu-TeUa-ruk,2501
|
41
|
+
aiagents4pharma/talk2competitors/config/__init__.py,sha256=HyM6paOpKZ5_tZnyVheSAFmxjT6Mb3PxvWKfP0rz-dE,113
|
42
|
+
aiagents4pharma/talk2competitors/config/config.py,sha256=jd4ltMBJyTztm9wT7j3ujOyYxL2SXRgxQJ4OZUBmCG4,5387
|
43
|
+
aiagents4pharma/talk2competitors/state/__init__.py,sha256=DzFjV3hZNes_pL4bDW2_8RsyK9BJcj6ejfBzU0KWn1k,106
|
44
|
+
aiagents4pharma/talk2competitors/state/state_talk2competitors.py,sha256=GUl1ZfM77XsjIEu-3xy4dtvaiMTA1pXf6i1ozVcX5Gg,993
|
45
|
+
aiagents4pharma/talk2competitors/tests/__init__.py,sha256=U3PsTiUZaUBD1IZanFGkDIOdFieDVJtGKQ5-woYUo8c,45
|
46
|
+
aiagents4pharma/talk2competitors/tests/test_langgraph.py,sha256=sEROK1aU3wFqJhZohONVI6Pr7t1d3PSqs-4erVIyiJw,9283
|
47
|
+
aiagents4pharma/talk2competitors/tools/__init__.py,sha256=YudBDRwaEzDnAcpxGZvEOfyh5-6xd51CTvTKTkywgXw,68
|
48
|
+
aiagents4pharma/talk2competitors/tools/s2/__init__.py,sha256=9RQH3efTj6qkXk0ICKSc7Mzpkitt4gRGsQ1pGPrrREU,181
|
49
|
+
aiagents4pharma/talk2competitors/tools/s2/display_results.py,sha256=B8JJGohi1Eyx8C3MhO_SiyQP3R6hPyUKJOAzcHmq3FU,584
|
50
|
+
aiagents4pharma/talk2competitors/tools/s2/multi_paper_rec.py,sha256=FYLt47DAk6WOKfEk1Gj9zVvJGNyxA283PCp8IKW9U5M,4262
|
51
|
+
aiagents4pharma/talk2competitors/tools/s2/search.py,sha256=pppjrQv5-8ep4fnqgTSBNgnbSnQsVIcNrRrH0p2TP1o,4025
|
52
|
+
aiagents4pharma/talk2competitors/tools/s2/single_paper_rec.py,sha256=dAfUQxI7T5eu0eDxK8VAl7-JH0Wnw24CVkOQqwj-hXc,4810
|
38
53
|
aiagents4pharma/talk2knowledgegraphs/__init__.py,sha256=SW7Ys2A4eXyFtizNPdSw91SHOPVUBGBsrCQ7TqwSUL0,91
|
39
54
|
aiagents4pharma/talk2knowledgegraphs/datasets/__init__.py,sha256=L3gPuHskSegmtXskVrLIYr7FXe_ibKgJ2GGr1_Wok6k,173
|
40
55
|
aiagents4pharma/talk2knowledgegraphs/datasets/biobridge_primekg.py,sha256=QlzDXmXREoa9MA6-GwzqRjdzndQeGBAF11Td6NFk_9Y,23426
|
@@ -55,8 +70,8 @@ aiagents4pharma/talk2knowledgegraphs/utils/embeddings/__init__.py,sha256=xRb0x7S
|
|
55
70
|
aiagents4pharma/talk2knowledgegraphs/utils/embeddings/embeddings.py,sha256=1nGznrAj-xT0xuSMBGz2dOujJ7M_IwSR84njxtxsy9A,2523
|
56
71
|
aiagents4pharma/talk2knowledgegraphs/utils/embeddings/huggingface.py,sha256=2vi_elf6EgzfagFAO5QnL3a_aXZyN7B1EBziu44MTfM,3806
|
57
72
|
aiagents4pharma/talk2knowledgegraphs/utils/embeddings/sentence_transformer.py,sha256=36iKlisOpMtGR5xfTAlSHXWvPqVC_Jbezod8kbBBMVg,2136
|
58
|
-
aiagents4pharma-1.
|
59
|
-
aiagents4pharma-1.
|
60
|
-
aiagents4pharma-1.
|
61
|
-
aiagents4pharma-1.
|
62
|
-
aiagents4pharma-1.
|
73
|
+
aiagents4pharma-1.10.0.dist-info/LICENSE,sha256=IcIbyB1Hyk5ZDah03VNQvJkbNk2hkBCDqQ8qtnCvB4Q,1077
|
74
|
+
aiagents4pharma-1.10.0.dist-info/METADATA,sha256=a5XUji4VHk7HcE5GC7txe7v2sNUbgH4ijSHpxoNh74E,8340
|
75
|
+
aiagents4pharma-1.10.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
76
|
+
aiagents4pharma-1.10.0.dist-info/top_level.txt,sha256=-AH8rMmrSnJtq7HaAObS78UU-cTCwvX660dSxeM7a0A,16
|
77
|
+
aiagents4pharma-1.10.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|