ursa-ai 0.4.2__tar.gz → 0.6.0__tar.gz

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 (50) hide show
  1. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/PKG-INFO +123 -4
  2. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/README.md +121 -1
  3. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/pyproject.toml +18 -2
  4. ursa_ai-0.6.0/src/ursa/__init__.py +0 -0
  5. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/agents/__init__.py +2 -0
  6. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/agents/arxiv_agent.py +88 -99
  7. ursa_ai-0.6.0/src/ursa/agents/base.py +408 -0
  8. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/agents/code_review_agent.py +3 -1
  9. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/agents/execution_agent.py +92 -48
  10. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/agents/hypothesizer_agent.py +39 -42
  11. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/agents/lammps_agent.py +51 -29
  12. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/agents/mp_agent.py +45 -20
  13. ursa_ai-0.6.0/src/ursa/agents/optimization_agent.py +405 -0
  14. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/agents/planning_agent.py +63 -28
  15. ursa_ai-0.6.0/src/ursa/agents/rag_agent.py +303 -0
  16. ursa_ai-0.6.0/src/ursa/agents/recall_agent.py +53 -0
  17. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/agents/websearch_agent.py +44 -54
  18. ursa_ai-0.6.0/src/ursa/cli/__init__.py +127 -0
  19. ursa_ai-0.6.0/src/ursa/cli/hitl.py +426 -0
  20. ursa_ai-0.6.0/src/ursa/observability/pricing.py +319 -0
  21. ursa_ai-0.6.0/src/ursa/observability/timing.py +1441 -0
  22. ursa_ai-0.6.0/src/ursa/prompt_library/__init__.py +0 -0
  23. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/prompt_library/execution_prompts.py +7 -0
  24. ursa_ai-0.6.0/src/ursa/prompt_library/optimization_prompts.py +131 -0
  25. ursa_ai-0.6.0/src/ursa/tools/__init__.py +0 -0
  26. ursa_ai-0.6.0/src/ursa/tools/feasibility_checker.py +114 -0
  27. ursa_ai-0.6.0/src/ursa/tools/feasibility_tools.py +1075 -0
  28. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/tools/write_code.py +1 -1
  29. ursa_ai-0.6.0/src/ursa/util/__init__.py +0 -0
  30. ursa_ai-0.6.0/src/ursa/util/helperFunctions.py +142 -0
  31. ursa_ai-0.6.0/src/ursa/util/optimization_schema.py +78 -0
  32. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/util/parse.py +1 -1
  33. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa_ai.egg-info/PKG-INFO +123 -4
  34. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa_ai.egg-info/SOURCES.txt +16 -0
  35. ursa_ai-0.6.0/src/ursa_ai.egg-info/entry_points.txt +2 -0
  36. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa_ai.egg-info/requires.txt +1 -2
  37. ursa_ai-0.4.2/src/ursa/agents/base.py +0 -41
  38. ursa_ai-0.4.2/src/ursa/agents/recall_agent.py +0 -23
  39. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/LICENSE +0 -0
  40. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/setup.cfg +0 -0
  41. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/prompt_library/code_review_prompts.py +0 -0
  42. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/prompt_library/hypothesizer_prompts.py +0 -0
  43. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/prompt_library/literature_prompts.py +0 -0
  44. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/prompt_library/planning_prompts.py +0 -0
  45. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/prompt_library/websearch_prompts.py +0 -0
  46. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/tools/run_command.py +0 -0
  47. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/util/diff_renderer.py +0 -0
  48. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa/util/memory_logger.py +0 -0
  49. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa_ai.egg-info/dependency_links.txt +0 -0
  50. {ursa_ai-0.4.2 → ursa_ai-0.6.0}/src/ursa_ai.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ursa-ai
3
- Version: 0.4.2
3
+ Version: 0.6.0
4
4
  Summary: Agents for science at LANL
5
5
  Author-email: Mike Grosskopf <mikegros@lanl.gov>, Nathan Debardeleben <ndebard@lanl.gov>, Rahul Somasundaram <rsomasundaram@lanl.gov>, Isaac Michaud <imichaud@lanl.gov>, Avanish Mishra <avanish@lanl.gov>, Arthur Lui <alui@lanl.gov>, Russell Bent <rbent@lanl.gov>, Earl Lawrence <earl@lanl.gov>
6
6
  License-Expression: BSD-3-Clause
@@ -38,8 +38,7 @@ Requires-Dist: langchain-anthropic<0.4,>=0.3.19
38
38
  Requires-Dist: langgraph-checkpoint-sqlite<3.0,>=2.0.10
39
39
  Requires-Dist: langchain-ollama<0.4,>=0.3.6
40
40
  Requires-Dist: ddgs>=9.5.5
41
- Requires-Dist: atomman>=1.5.2
42
- Requires-Dist: trafilatura>=1.6.1
41
+ Requires-Dist: typer>=0.16.1
43
42
  Dynamic: license-file
44
43
 
45
44
  # URSA - The Universal Research and Scientific Agent
@@ -81,7 +80,68 @@ Documentation for combining agents:
81
80
  - [ArXiv -> Execution for Materials](docs/combining_arxiv_and_execution.md)
82
81
  - [ArXiv -> Execution for Neutron Star Properties](docs/combining_arxiv_and_execution_neutronStar.md)
83
82
 
84
- # Sandboxing
83
+
84
+ ## Command line usage
85
+
86
+ You can install `ursa` as a command line app with `pip install`; or with `uv` via
87
+
88
+ ```bash
89
+ uv tool install ursa-ai
90
+ ```
91
+
92
+ To use the command line app, run
93
+
94
+ ```
95
+ ursa run
96
+ ```
97
+
98
+ This will start a REPL in your terminal.
99
+
100
+ ```
101
+ __ ________________ _
102
+ / / / / ___/ ___/ __ `/
103
+ / /_/ / / (__ ) /_/ /
104
+ \__,_/_/ /____/\__,_/
105
+
106
+ For help, type: ? or help. Exit with Ctrl+d.
107
+ ursa>
108
+ ```
109
+
110
+ Within the REPL, you can get help by typing `?` or `help`.
111
+
112
+ You can chat with an LLM by simply typing into the terminal.
113
+
114
+ ```
115
+ ursa> How are you?
116
+ Thanks for asking! I’m doing well. How are you today? What can I help you with?
117
+ ```
118
+
119
+ You can run various agents by typing the name of the agent. For example,
120
+
121
+ ```
122
+ ursa> plan
123
+ Enter your prompt for Planning Agent: Write a python script to do linear regression using only numpy.
124
+ ```
125
+
126
+ If you run subsequent agents, the last output will be appended to the prompt for the next agent.
127
+
128
+ So, to run the Planning Agent followed by the Execution Agent:
129
+ ```
130
+ ursa> plan
131
+ Enter your prompt for Planning Agent: Write a python script to do linear regression using only numpy.
132
+
133
+ ...
134
+
135
+ ursa> execute
136
+ Enter your prompt for Execution Agent: Execute the plan.
137
+ ```
138
+
139
+ You can get a list of available command line options via
140
+ ```
141
+ ursa run --help
142
+ ```
143
+
144
+ ## Sandboxing
85
145
  The Execution Agent is allowed to run system commands and write/run code. Being able to execute arbitrary system commands or write
86
146
  and execute code has the potential to cause problems like:
87
147
  - Damage code or data on the computer
@@ -98,6 +158,65 @@ Some suggestions for sandboxing the agent:
98
158
 
99
159
  You have a duty for ensuring that you use URSA responsibly.
100
160
 
161
+ ## Container image
162
+
163
+ To enable limited sandboxing insofar as containerization does this, you can run
164
+ the following commands:
165
+
166
+ ### Docker
167
+
168
+ ```shell
169
+ # Build a local container using the Docker runtime
170
+ docker buildx build --progress=plain -t ursa .
171
+
172
+ # Run included example
173
+ docker run -e "OPENAI_API_KEY"=$OPENAI_API_KEY ursa \
174
+ bash -c "uv run python examples/single_agent_examples/execution_agnet/integer_sum.py"
175
+
176
+ # Run script from host system
177
+ mkdir -p scripts
178
+ echo "import ursa; print('Hello from ursa')" > scripts/my_script.py
179
+ docker run -e "OPENAI_API_KEY"=$OPENAI_API_KEY \
180
+ --mount type=bind,src=$PWD/scripts,dst=/mnt/workspace \
181
+ ursa \
182
+ bash -c "uv run /mnt/workspace/my_script.py"
183
+ ```
184
+
185
+ ### Charliecloud
186
+
187
+ [Charliecloud](https://charliecloud.io/) is a rootless alternative to docker
188
+ that is sometimes preferred on HPC. The following commands replicate the
189
+ behaviors above for docker.
190
+
191
+ ```shell
192
+ # Build a local container using the Docker runtime
193
+ ch-image build -t ursa
194
+
195
+ # Convert image to sqfs, for use on another system
196
+ ch-convert ursa ursa.sqfs
197
+
198
+ # Run included example (if wanted, replace ursa with /path/to/ursa.sqfs)
199
+ ch-run -W ursa \
200
+ --unset-env="*" \
201
+ --set-env \
202
+ --set-env="OPENAI_API_KEY"=$OPENAI_API_KEY \
203
+ --cd /app \
204
+ -- bash -c \
205
+ "uv run python examples/single_agent_examples/execution_agnet/integer_sum.py"
206
+
207
+ # Run script from host system (if wanted, replace ursa with /path/to/ursa.sqfs)
208
+ mkdir -p scripts
209
+ echo "import ursa; print('Hello from ursa')" > scripts/my_script.py
210
+ ch-run -W ursa \
211
+ --unset-env="*" \
212
+ --set-env \
213
+ --set-env="OPENAI_API_KEY"=$OPENAI_API_KEY \
214
+ --bind ${PWD}/scripts:/mnt/workspace \
215
+ --cd /app \
216
+ -- bash -c \
217
+ "uv run python /mnt/workspace/integer_sum.py"
218
+ ```
219
+
101
220
  ## Development Dependencies
102
221
 
103
222
  * [`uv`](https://docs.astral.sh/uv/)
@@ -37,7 +37,68 @@ Documentation for combining agents:
37
37
  - [ArXiv -> Execution for Materials](docs/combining_arxiv_and_execution.md)
38
38
  - [ArXiv -> Execution for Neutron Star Properties](docs/combining_arxiv_and_execution_neutronStar.md)
39
39
 
40
- # Sandboxing
40
+
41
+ ## Command line usage
42
+
43
+ You can install `ursa` as a command line app with `pip install`; or with `uv` via
44
+
45
+ ```bash
46
+ uv tool install ursa-ai
47
+ ```
48
+
49
+ To use the command line app, run
50
+
51
+ ```
52
+ ursa run
53
+ ```
54
+
55
+ This will start a REPL in your terminal.
56
+
57
+ ```
58
+ __ ________________ _
59
+ / / / / ___/ ___/ __ `/
60
+ / /_/ / / (__ ) /_/ /
61
+ \__,_/_/ /____/\__,_/
62
+
63
+ For help, type: ? or help. Exit with Ctrl+d.
64
+ ursa>
65
+ ```
66
+
67
+ Within the REPL, you can get help by typing `?` or `help`.
68
+
69
+ You can chat with an LLM by simply typing into the terminal.
70
+
71
+ ```
72
+ ursa> How are you?
73
+ Thanks for asking! I’m doing well. How are you today? What can I help you with?
74
+ ```
75
+
76
+ You can run various agents by typing the name of the agent. For example,
77
+
78
+ ```
79
+ ursa> plan
80
+ Enter your prompt for Planning Agent: Write a python script to do linear regression using only numpy.
81
+ ```
82
+
83
+ If you run subsequent agents, the last output will be appended to the prompt for the next agent.
84
+
85
+ So, to run the Planning Agent followed by the Execution Agent:
86
+ ```
87
+ ursa> plan
88
+ Enter your prompt for Planning Agent: Write a python script to do linear regression using only numpy.
89
+
90
+ ...
91
+
92
+ ursa> execute
93
+ Enter your prompt for Execution Agent: Execute the plan.
94
+ ```
95
+
96
+ You can get a list of available command line options via
97
+ ```
98
+ ursa run --help
99
+ ```
100
+
101
+ ## Sandboxing
41
102
  The Execution Agent is allowed to run system commands and write/run code. Being able to execute arbitrary system commands or write
42
103
  and execute code has the potential to cause problems like:
43
104
  - Damage code or data on the computer
@@ -54,6 +115,65 @@ Some suggestions for sandboxing the agent:
54
115
 
55
116
  You have a duty for ensuring that you use URSA responsibly.
56
117
 
118
+ ## Container image
119
+
120
+ To enable limited sandboxing insofar as containerization does this, you can run
121
+ the following commands:
122
+
123
+ ### Docker
124
+
125
+ ```shell
126
+ # Build a local container using the Docker runtime
127
+ docker buildx build --progress=plain -t ursa .
128
+
129
+ # Run included example
130
+ docker run -e "OPENAI_API_KEY"=$OPENAI_API_KEY ursa \
131
+ bash -c "uv run python examples/single_agent_examples/execution_agnet/integer_sum.py"
132
+
133
+ # Run script from host system
134
+ mkdir -p scripts
135
+ echo "import ursa; print('Hello from ursa')" > scripts/my_script.py
136
+ docker run -e "OPENAI_API_KEY"=$OPENAI_API_KEY \
137
+ --mount type=bind,src=$PWD/scripts,dst=/mnt/workspace \
138
+ ursa \
139
+ bash -c "uv run /mnt/workspace/my_script.py"
140
+ ```
141
+
142
+ ### Charliecloud
143
+
144
+ [Charliecloud](https://charliecloud.io/) is a rootless alternative to docker
145
+ that is sometimes preferred on HPC. The following commands replicate the
146
+ behaviors above for docker.
147
+
148
+ ```shell
149
+ # Build a local container using the Docker runtime
150
+ ch-image build -t ursa
151
+
152
+ # Convert image to sqfs, for use on another system
153
+ ch-convert ursa ursa.sqfs
154
+
155
+ # Run included example (if wanted, replace ursa with /path/to/ursa.sqfs)
156
+ ch-run -W ursa \
157
+ --unset-env="*" \
158
+ --set-env \
159
+ --set-env="OPENAI_API_KEY"=$OPENAI_API_KEY \
160
+ --cd /app \
161
+ -- bash -c \
162
+ "uv run python examples/single_agent_examples/execution_agnet/integer_sum.py"
163
+
164
+ # Run script from host system (if wanted, replace ursa with /path/to/ursa.sqfs)
165
+ mkdir -p scripts
166
+ echo "import ursa; print('Hello from ursa')" > scripts/my_script.py
167
+ ch-run -W ursa \
168
+ --unset-env="*" \
169
+ --set-env \
170
+ --set-env="OPENAI_API_KEY"=$OPENAI_API_KEY \
171
+ --bind ${PWD}/scripts:/mnt/workspace \
172
+ --cd /app \
173
+ -- bash -c \
174
+ "uv run python /mnt/workspace/integer_sum.py"
175
+ ```
176
+
57
177
  ## Development Dependencies
58
178
 
59
179
  * [`uv`](https://docs.astral.sh/uv/)
@@ -38,8 +38,7 @@ dependencies = [
38
38
  "langgraph-checkpoint-sqlite>=2.0.10,<3.0",
39
39
  "langchain-ollama>=0.3.6,<0.4",
40
40
  "ddgs>=9.5.5",
41
- "atomman>=1.5.2",
42
- "trafilatura>=1.6.1",
41
+ "typer>=0.16.1",
43
42
  ]
44
43
  classifiers = [
45
44
  "Operating System :: OS Independent",
@@ -50,6 +49,9 @@ classifiers = [
50
49
  "Programming Language :: Python :: 3.14",
51
50
  ]
52
51
 
52
+ [project.scripts]
53
+ ursa = "ursa.cli:main"
54
+
53
55
  [project.urls]
54
56
  Homepage = "https://github.com/lanl/ursa"
55
57
  Documentation = "https://github.com/lanl/ursa/tree/main/docs"
@@ -81,5 +83,19 @@ dev = [
81
83
  "langgraph-checkpoint-sqlite>=2.0.10",
82
84
  "notebook>=7.3.3",
83
85
  "pre-commit>=4.3.0",
86
+ "pytest>=8.4.2",
84
87
  "scikit-optimize>=0.10.2",
85
88
  ]
89
+ docs = [
90
+ "mkdocs>=1.6.1",
91
+ "mkdocs-autorefs>=1.4.3",
92
+ "mkdocs-material>=9.6.21",
93
+ "mkdocstrings-python>=1.18.2",
94
+ ]
95
+ lammps = [
96
+ "atomman>=1.5.2",
97
+ "trafilatura>=1.6.1",
98
+ ]
99
+ opt = [
100
+ "ortools>=9.14,<9.15",
101
+ ]
File without changes
@@ -14,6 +14,8 @@ from .lammps_agent import LammpsState as LammpsState
14
14
  from .mp_agent import MaterialsProjectAgent as MaterialsProjectAgent
15
15
  from .planning_agent import PlanningAgent as PlanningAgent
16
16
  from .planning_agent import PlanningState as PlanningState
17
+ from .rag_agent import RAGAgent as RAGAgent
18
+ from .rag_agent import RAGState as RAGState
17
19
  from .recall_agent import RecallAgent as RecallAgent
18
20
  from .websearch_agent import WebSearchAgent as WebSearchAgent
19
21
  from .websearch_agent import WebSearchState as WebSearchState
@@ -1,17 +1,16 @@
1
1
  import base64
2
2
  import os
3
3
  import re
4
- import statistics
5
4
  from concurrent.futures import ThreadPoolExecutor, as_completed
6
5
  from io import BytesIO
6
+ from typing import Any, Mapping
7
7
  from urllib.parse import quote
8
8
 
9
9
  import feedparser
10
10
  import pymupdf
11
11
  import requests
12
- from langchain.text_splitter import RecursiveCharacterTextSplitter
13
- from langchain_chroma import Chroma
14
12
  from langchain_community.document_loaders import PyPDFLoader
13
+ from langchain_core.language_models import BaseChatModel
15
14
  from langchain_core.output_parsers import StrOutputParser
16
15
  from langchain_core.prompts import ChatPromptTemplate
17
16
  from langgraph.graph import StateGraph
@@ -19,16 +18,14 @@ from PIL import Image
19
18
  from tqdm import tqdm
20
19
  from typing_extensions import List, TypedDict
21
20
 
22
- from .base import BaseAgent
21
+ from ursa.agents.base import BaseAgent
22
+ from ursa.agents.rag_agent import RAGAgent
23
23
 
24
24
  try:
25
25
  from openai import OpenAI
26
26
  except Exception:
27
27
  pass
28
28
 
29
- # embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001")
30
- # embeddings = OpenAIEmbeddings()
31
-
32
29
 
33
30
  class PaperMetadata(TypedDict):
34
31
  arxiv_id: str
@@ -125,7 +122,7 @@ def remove_surrogates(text: str) -> str:
125
122
  class ArxivAgent(BaseAgent):
126
123
  def __init__(
127
124
  self,
128
- llm="openai/o3-mini",
125
+ llm: str | BaseChatModel = "openai/o3-mini",
129
126
  summarize: bool = True,
130
127
  process_images=True,
131
128
  max_results: int = 3,
@@ -146,7 +143,7 @@ class ArxivAgent(BaseAgent):
146
143
  self.download_papers = download_papers
147
144
  self.rag_embedding = rag_embedding
148
145
 
149
- self.graph = self._build_graph()
146
+ self._action = self._build_graph()
150
147
 
151
148
  os.makedirs(self.database_path, exist_ok=True)
152
149
 
@@ -242,27 +239,6 @@ class ArxivAgent(BaseAgent):
242
239
  papers = self._fetch_papers(state["query"])
243
240
  return {**state, "papers": papers}
244
241
 
245
- def _get_or_build_vectorstore(self, paper_text: str, arxiv_id: str):
246
- os.makedirs(self.vectorstore_path, exist_ok=True)
247
-
248
- persist_directory = os.path.join(self.vectorstore_path, arxiv_id)
249
-
250
- if os.path.exists(persist_directory):
251
- vectorstore = Chroma(
252
- persist_directory=persist_directory,
253
- embedding_function=self.rag_embedding,
254
- )
255
- else:
256
- splitter = RecursiveCharacterTextSplitter(
257
- chunk_size=1000, chunk_overlap=200
258
- )
259
- docs = splitter.create_documents([paper_text])
260
- vectorstore = Chroma.from_documents(
261
- docs, self.rag_embedding, persist_directory=persist_directory
262
- )
263
-
264
- return vectorstore.as_retriever(search_kwargs={"k": 5})
265
-
266
242
  def _summarize_node(self, state: PaperState) -> PaperState:
267
243
  prompt = ChatPromptTemplate.from_template("""
268
244
  You are a scientific assistant responsible for summarizing extracts from research papers, in the context of the following task: {context}
@@ -285,35 +261,13 @@ class ArxivAgent(BaseAgent):
285
261
 
286
262
  try:
287
263
  cleaned_text = remove_surrogates(paper["full_text"])
288
- if self.rag_embedding:
289
- retriever = self._get_or_build_vectorstore(
290
- cleaned_text, arxiv_id
291
- )
292
-
293
- relevant_docs_with_scores = (
294
- retriever.vectorstore.similarity_search_with_score(
295
- state["context"], k=5
296
- )
297
- )
298
-
299
- if relevant_docs_with_scores:
300
- score = sum([
301
- s for _, s in relevant_docs_with_scores
302
- ]) / len(relevant_docs_with_scores)
303
- relevancy_scores[i] = abs(1.0 - score)
304
- else:
305
- relevancy_scores[i] = 0.0
306
-
307
- retrieved_content = "\n\n".join([
308
- doc.page_content for doc, _ in relevant_docs_with_scores
309
- ])
310
- else:
311
- retrieved_content = cleaned_text
312
-
313
- summary = chain.invoke({
314
- "retrieved_content": retrieved_content,
315
- "context": state["context"],
316
- })
264
+ summary = chain.invoke(
265
+ {
266
+ "retrieved_content": cleaned_text,
267
+ "context": state["context"],
268
+ },
269
+ config=self.build_config(tags=["arxiv", "summarize_each"]),
270
+ )
317
271
 
318
272
  except Exception as e:
319
273
  summary = f"Error summarizing paper: {e}"
@@ -346,15 +300,20 @@ class ArxivAgent(BaseAgent):
346
300
  i, result = future.result()
347
301
  summaries[i] = result
348
302
 
349
- if self.rag_embedding:
350
- print(f"\nMax Relevancy Score: {max(relevancy_scores)}")
351
- print(f"Min Relevancy Score: {min(relevancy_scores)}")
352
- print(
353
- f"Median Relevancy Score: {statistics.median(relevancy_scores)}\n"
354
- )
355
-
356
303
  return {**state, "summaries": summaries}
357
304
 
305
+ def _rag_node(self, state: PaperState) -> PaperState:
306
+ new_state = state.copy()
307
+ rag_agent = RAGAgent(
308
+ llm=self.llm,
309
+ embedding=self.rag_embedding,
310
+ database_path=self.database_path,
311
+ )
312
+ new_state["final_summary"] = rag_agent.invoke(context=state["context"])[
313
+ "summary"
314
+ ]
315
+ return new_state
316
+
358
317
  def _aggregate_node(self, state: PaperState) -> PaperState:
359
318
  summaries = state["summaries"]
360
319
  papers = state["papers"]
@@ -389,10 +348,13 @@ class ArxivAgent(BaseAgent):
389
348
 
390
349
  chain = prompt | self.llm | StrOutputParser()
391
350
 
392
- final_summary = chain.invoke({
393
- "Summaries": combined,
394
- "context": state["context"],
395
- })
351
+ final_summary = chain.invoke(
352
+ {
353
+ "Summaries": combined,
354
+ "context": state["context"],
355
+ },
356
+ config=self.build_config(tags=["arxiv", "aggregate"]),
357
+ )
396
358
 
397
359
  with open(self.summaries_path + "/final_summary.txt", "w") as f:
398
360
  f.write(final_summary)
@@ -400,42 +362,69 @@ class ArxivAgent(BaseAgent):
400
362
  return {**state, "final_summary": final_summary}
401
363
 
402
364
  def _build_graph(self):
403
- builder = StateGraph(PaperState)
404
- builder.add_node("fetch_papers", self._fetch_node)
365
+ graph = StateGraph(PaperState)
405
366
 
367
+ self.add_node(graph, self._fetch_node)
406
368
  if self.summarize:
407
- builder.add_node("summarize_each", self._summarize_node)
408
- builder.add_node("aggregate", self._aggregate_node)
369
+ if self.rag_embedding:
370
+ self.add_node(graph, self._rag_node)
371
+ graph.set_entry_point("_fetch_node")
372
+ graph.add_edge("_fetch_node", "_rag_node")
373
+ graph.set_finish_point("_rag_node")
374
+ else:
375
+ self.add_node(graph, self._summarize_node)
376
+ self.add_node(graph, self._aggregate_node)
377
+
378
+ graph.set_entry_point("_fetch_node")
379
+ graph.add_edge("_fetch_node", "_summarize_node")
380
+ graph.add_edge("_summarize_node", "_aggregate_node")
381
+ graph.set_finish_point("_aggregate_node")
382
+ else:
383
+ graph.set_entry_point("_fetch_node")
384
+ graph.set_finish_point("_fetch_node")
409
385
 
410
- builder.set_entry_point("fetch_papers")
411
- builder.add_edge("fetch_papers", "summarize_each")
412
- builder.add_edge("summarize_each", "aggregate")
413
- builder.set_finish_point("aggregate")
386
+ return graph.compile(checkpointer=self.checkpointer)
414
387
 
415
- else:
416
- builder.set_entry_point("fetch_papers")
417
- builder.set_finish_point("fetch_papers")
388
+ def _invoke(
389
+ self,
390
+ inputs: Mapping[str, Any],
391
+ *,
392
+ summarize: bool | None = None,
393
+ recursion_limit: int = 1000,
394
+ **_,
395
+ ) -> str:
396
+ config = self.build_config(
397
+ recursion_limit=recursion_limit, tags=["graph"]
398
+ )
418
399
 
419
- graph = builder.compile()
420
- return graph
400
+ # this seems dumb, but it's b/c sometimes we had referred to the value as
401
+ # 'query' other times as 'arxiv_search_query' so trying to keep it compatible
402
+ # aliasing: accept arxiv_search_query -> query
403
+ if "query" not in inputs:
404
+ if "arxiv_search_query" in inputs:
405
+ # make a shallow copy and rename the key
406
+ inputs = dict(inputs)
407
+ inputs["query"] = inputs.pop("arxiv_search_query")
408
+ else:
409
+ raise KeyError(
410
+ "Missing 'query' in inputs (alias 'arxiv_search_query' also accepted)."
411
+ )
421
412
 
422
- def run(self, arxiv_search_query: str, context: str) -> str:
423
- result = self.graph.invoke({
424
- "query": arxiv_search_query,
425
- "context": context,
426
- })
413
+ result = self._action.invoke(inputs, config)
427
414
 
428
- if self.summarize:
429
- return result.get("final_summary", "No summary generated.")
430
- else:
431
- return "\n\nFinished Fetching papers!"
415
+ use_summary = self.summarize if summarize is None else summarize
432
416
 
417
+ return (
418
+ result.get("final_summary", "No summary generated.")
419
+ if use_summary
420
+ else "\n\nFinished Fetching papers!"
421
+ )
433
422
 
434
- if __name__ == "__main__":
435
- agent = ArxivAgent()
436
- result = agent.run(
437
- arxiv_search_query="Experimental Constraints on neutron star radius",
438
- context="What are the constraints on the neutron star radius and what uncertainties are there on the constraints?",
439
- )
440
423
 
441
- print(result)
424
+ # NOTE: Run test in `tests/agents/test_arxiv_agent/test_arxiv_agent.py` via:
425
+ #
426
+ # pytest -s tests/agents/test_arxiv_agent
427
+ #
428
+ # OR
429
+ #
430
+ # uv run pytest -s tests/agents/test_arxiv_agent