aviary.hotpotqa 0.1.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.
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.1
2
+ Name: aviary.hotpotqa
3
+ Version: 0.1.0
4
+ Summary: HotPotQA environment implemented with aviary
5
+ Author-email: FutureHouse technical staff <hello@futurehouse.org>
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: beautifulsoup4
9
+ Requires-Dist: datasets
10
+ Requires-Dist: fhaviary
11
+ Requires-Dist: httpx
12
+ Requires-Dist: pydantic~=2.0
13
+ Requires-Dist: tenacity
14
+
15
+ # aviary.hotpotqa
16
+
17
+ HotPotQA environment implemented with aviary.
18
+
19
+ ## References
20
+
21
+ [1] Yang et al. [HotpotQA: A Dataset for Diverse,
22
+ Explainable Multi-Hop Question Answering](https://aclanthology.org/D18-1259/). EMNLP, 2018.
23
+
24
+ [2] Yao et al.,
25
+ [ReAct: Synergizing Reasoning and Acting in Language Models](https://openreview.net/forum?id=WE_vluYUL-X).
26
+ In The Eleventh International Conference on Learning Representations. 2023
@@ -0,0 +1,12 @@
1
+ # aviary.hotpotqa
2
+
3
+ HotPotQA environment implemented with aviary.
4
+
5
+ ## References
6
+
7
+ [1] Yang et al. [HotpotQA: A Dataset for Diverse,
8
+ Explainable Multi-Hop Question Answering](https://aclanthology.org/D18-1259/). EMNLP, 2018.
9
+
10
+ [2] Yao et al.,
11
+ [ReAct: Synergizing Reasoning and Acting in Language Models](https://openreview.net/forum?id=WE_vluYUL-X).
12
+ In The Eleventh International Conference on Learning Representations. 2023
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = ["setuptools>=64", "setuptools_scm>=8"]
4
+
5
+ [project]
6
+ authors = [
7
+ {email = "hello@futurehouse.org", name = "FutureHouse technical staff"},
8
+ ]
9
+ dependencies = [
10
+ "beautifulsoup4",
11
+ "datasets",
12
+ "fhaviary",
13
+ "httpx",
14
+ "pydantic~=2.0",
15
+ "tenacity",
16
+ ]
17
+ description = "HotPotQA environment implemented with aviary"
18
+ dynamic = ["version"]
19
+ name = "aviary.hotpotqa"
20
+ readme = "README.md"
21
+ requires-python = ">=3.11"
22
+
23
+ [tool.ruff]
24
+ extend = "../../pyproject.toml"
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["src"]
28
+
29
+ [tool.setuptools_scm]
30
+ root = "../.."
31
+ version_file = "src/aviary/hotpotqa/version.py"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from .env import HotPotQADataset, HotPotQAEnv, HotPotQAEnvConfig, HotPotQAEnvState
2
+
3
+ __all__ = ["HotPotQADataset", "HotPotQAEnv", "HotPotQAEnvConfig", "HotPotQAEnvState"]
@@ -0,0 +1,559 @@
1
+ """HotPotQA environment for aviary agents.
2
+
3
+ Implements the HotPotQA multihop question-answering environment from:
4
+
5
+ Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models.
6
+ In The Eleventh International Conference on Learning Representations. 2023
7
+
8
+ Agents in the HotPotQA environment can perform searches and lookups to retrieve specific information.
9
+ The environment supports 3 tools:
10
+
11
+ Search[entity]: Search for a specific entity on Wikipedia and return relevant information.
12
+ Lookup[keyword]: Find and return the next sentence containing the keyword in the current passage.
13
+ Finish[answer]: Submit the final answer to the question and conclude the task.
14
+ """
15
+
16
+ import logging
17
+ import random
18
+ import re
19
+ import string
20
+ from collections.abc import Callable
21
+ from enum import StrEnum
22
+ from typing import Any, ClassVar, cast
23
+
24
+ import httpx
25
+ from bs4 import BeautifulSoup
26
+ from datasets import load_dataset
27
+ from pydantic import BaseModel, ConfigDict, Field
28
+ from tenacity import retry, stop_after_attempt, wait_exponential_jitter
29
+
30
+ from aviary.env import Environment, Frame, TaskDataset
31
+ from aviary.message import Message
32
+ from aviary.tools import Tool, ToolRequestMessage, ToolResponseMessage
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ # Jitter in case we have lots of requests at once, to space them out slightly
38
+ @retry(stop=stop_after_attempt(2), wait=wait_exponential_jitter(initial=1, max=4))
39
+ async def fetch_with_retry(
40
+ client: httpx.AsyncClient, url: str, **kwargs
41
+ ) -> httpx.Response:
42
+ response = await client.get(url, **kwargs)
43
+ if response.status_code == httpx.codes.TOO_MANY_REQUESTS:
44
+ response.raise_for_status()
45
+ return response
46
+
47
+
48
+ class HotPotQAEnvState(BaseModel):
49
+ """State of the HotPotQA environment."""
50
+
51
+ done: bool = Field(
52
+ default=False,
53
+ description="Flag for being done, automatically set true after many steps.",
54
+ )
55
+
56
+ steps: int = Field(default=0, description="Count of environment steps.")
57
+
58
+ reward: float = Field(
59
+ default=0.0, description="Current reward value, reset each environment step."
60
+ )
61
+
62
+ answer: str | None = Field(
63
+ default=None,
64
+ description="The answer to the question, or None if not yet answered.",
65
+ )
66
+
67
+ last_lookup: str | None = Field(
68
+ default=None, description="The last lookup keyword."
69
+ )
70
+ lookup_results: list[str] = Field(
71
+ default_factory=list, description="Results of the last lookup."
72
+ )
73
+ lookup_index: int = Field(
74
+ default=0, description="Index of the last retrieved lookup result."
75
+ )
76
+ page: str | None = Field(default=None, description="The current Wikipedia page.")
77
+
78
+
79
+ def create_tool(function: Callable, name: str) -> Tool:
80
+ """Create a Tool object from a function and set its name.
81
+
82
+ Args:
83
+ function: The function to be wrapped by the Tool.
84
+ name: The name to assign to the Tool.
85
+
86
+ Returns:
87
+ A Tool object with the specified function and name.
88
+ """
89
+ tool = Tool.from_function(function)
90
+ tool.info.name = name
91
+ return tool
92
+
93
+
94
+ def clean_str(p: str) -> str:
95
+ r"""Clean and normalize a given string by encoding and decoding it.
96
+
97
+ Args:
98
+ p: The input string to be cleaned and normalized.
99
+
100
+ Returns:
101
+ The cleaned and normalized UTF-8 encoded string.
102
+
103
+ Examples:
104
+ >>> clean_str("This is a test string with unicode escape: \\u00e9")
105
+ 'This is a test string with unicode escape: é'
106
+ >>> clean_str(r"SBN IT\ICCU\UBO\2771748.") # SEE: https://en.wikipedia.org/wiki/Tricolour_Day
107
+ 'SBN IT\\ICCU\\UBO\\2771748.'
108
+ """
109
+ # Original source:
110
+ # https://github.com/ysymyth/ReAct/blob/6bdb3a1fd38b8188fc7ba4102969fe483df8fdc9/wikienv.py#L10-L11
111
+ try:
112
+ # unicode-escape codec can interpret backslash escapes (e.g., `\n`, `\u1234`)
113
+ return (
114
+ p.encode("latin1").decode("unicode-escape").encode("utf-8").decode("utf-8")
115
+ )
116
+ except (UnicodeEncodeError, UnicodeDecodeError):
117
+ logger.debug(f"Replacing bad characters in input string {p!r}.")
118
+ return p.encode(errors="replace").decode("utf-8", errors="replace")
119
+
120
+
121
+ def normalize_answer(s: str | float) -> str:
122
+ """Normalize the given answer string by applying several text processing steps.
123
+
124
+ This function processes the input string through a series of transformations to
125
+ normalize it, which includes:
126
+ 1. Convert all characters to lowercase.
127
+ 2. Remove punctuation from the text.
128
+ 3. Remove articles ('a', 'an', 'the') from the text.
129
+ 4. Fix extra whitespace by reducing multiple spaces to a single space.
130
+
131
+ Args:
132
+ s: The input answer string to be normalized.
133
+
134
+ Returns:
135
+ The normalized answer string.
136
+
137
+ Example:
138
+ >>> normalize_answer("The Quick, Brown Fox!")
139
+ 'quick brown fox'
140
+ """
141
+ # if s is not a string, convert it to a string
142
+ if not isinstance(s, str):
143
+ return str(s)
144
+
145
+ def remove_articles(text: str) -> str:
146
+ """Remove articles ('a', 'an', 'the') from the text."""
147
+ return re.sub(r"\b(a|an|the)\b", " ", text)
148
+
149
+ def white_space_fix(text: str) -> str:
150
+ """Reduce multiple spaces to a single space."""
151
+ return " ".join(text.split())
152
+
153
+ def remove_punc(text: str) -> str:
154
+ """Remove all punctuation from the text."""
155
+ exclude = set(string.punctuation)
156
+ return "".join(ch for ch in text if ch not in exclude)
157
+
158
+ def lower(text: str) -> str:
159
+ """Convert all characters to lowercase."""
160
+ return text.lower()
161
+
162
+ return white_space_fix(remove_articles(remove_punc(lower(s))))
163
+
164
+
165
+ class HotPotQAEnv(Environment[HotPotQAEnvState]):
166
+ State: ClassVar = HotPotQAEnvState
167
+ wiki_cache: ClassVar[dict[str, str]] = {}
168
+
169
+ def __init__(
170
+ self,
171
+ question: str,
172
+ correct_answer: str,
173
+ correct_reward: float = 1.0,
174
+ incorrect_reward: float = 0.0,
175
+ tool_failure_reward: float = 0.0,
176
+ proxy: str | None = None,
177
+ ):
178
+ super().__init__()
179
+ self.question = question
180
+ self.correct_answer = correct_answer
181
+ self.correct_reward = correct_reward
182
+ self.incorrect_reward = incorrect_reward
183
+ self.tool_failure_reward = tool_failure_reward
184
+ self.proxy = proxy
185
+
186
+ # Title case tool names to match third party demonstration data
187
+ self.tools = [
188
+ create_tool(self.search, "Search"),
189
+ create_tool(self.construct_lookup_list, "Lookup"),
190
+ create_tool(self.finish, "Finish"),
191
+ ]
192
+
193
+ def calculate_reward(self, answer: str | None) -> float:
194
+ """Calculate the reward based on the agent's answer.
195
+
196
+ Returns:
197
+ The correct reward if the agent's answer is correct (prediction exactly
198
+ matches ground truth), otherwise the incorrect reward.
199
+ """
200
+ if answer is None:
201
+ return self.incorrect_reward
202
+ gt = normalize_answer(self.correct_answer)
203
+ pred = normalize_answer(answer)
204
+ return self.correct_reward if pred == gt else self.incorrect_reward
205
+
206
+ async def reset(self) -> tuple[list[Message], list[Tool]]:
207
+ """Reset the HotPotQA environment to an initial state.
208
+
209
+ This method resets the environment to its initial state, setting up necessary variables and tools
210
+ for the agent to interact with. It prepares the environment for a new episode by initializing
211
+ various attributes and returning the initial observation and tools available for the agent.
212
+
213
+ Returns:
214
+ tuple: A tuple containing:
215
+ - list[Message]: The initial observation wrapped in a Message object.
216
+ - list[Tool]: A list of tools (Search, Lookup, and Finish) available for the agent.
217
+
218
+ Example:
219
+ >>> env = HotPotQAEnv()
220
+ >>> initial_obs, tools = env.reset(seed=42, idx=5)
221
+ >>> print(initial_obs)
222
+ [Message(content='Question: <question_text>')]
223
+ >>> print(tools)
224
+ [<Tool: Search>, <Tool: Lookup>, <Tool: Finish>]
225
+ """
226
+ self.state = self.State()
227
+ return [Message(content=f"Question: {self.question}")], self.tools
228
+
229
+ async def step(
230
+ self, action: ToolRequestMessage
231
+ ) -> tuple[list[Message], float, bool, bool]:
232
+ """Take a step in the environment. Assume only one tool at a time can be called for HotpotQA.
233
+
234
+ This method processes an action message, which can be a tool request, a finish request, or an error message.
235
+ It updates the environment state accordingly and returns the observation and done status.
236
+
237
+ Args:
238
+ action: Action to take.
239
+
240
+ Returns:
241
+ Tuple[List[ToolResponseMessage], bool]: A tuple containing:
242
+ - list[ToolResponseMessage]: The response message(s) from the executed tool.
243
+ - bool: The done status indicating whether the episode is finished.
244
+
245
+ Example:
246
+ >>> env = HotPotQAEnv()
247
+ >>> action = ToolRequestMessage(
248
+ ... tool_calls=[
249
+ ... ToolCall(
250
+ ... function=ToolCallFunction(name="Search"),
251
+ ... arguments={"entity": "Python"},
252
+ ... )
253
+ ... ]
254
+ ... )
255
+ >>> obs, done = await env.step(action)
256
+ >>> print(obs, done)
257
+ [ToolResponseMessage(name='Search', tool_call_id='tool_call_id', content='...')], False
258
+ """
259
+ self.state.steps += 1
260
+ if not action.tool_calls:
261
+ return (
262
+ [
263
+ Message(
264
+ content=(
265
+ f"Must call one of the provided tools"
266
+ f" { {t.info.name for t in self.tools} }."
267
+ )
268
+ )
269
+ ],
270
+ self.tool_failure_reward,
271
+ self.state.done,
272
+ False,
273
+ )
274
+
275
+ # We accumulate reward across all tool calls in a step
276
+ self.state.reward = 0.0
277
+ valid_action, invalid_action = self.filter_invalid_tool_calls(action)
278
+ # NOTE: valid_action or invalid_action may have an empty list of tool calls
279
+ response_messages = cast(
280
+ list[Message],
281
+ # Ordered since things like search -> lookup need to be run in order.
282
+ # NOTE: Handling tool exceptions here keeps the trajectory going, but I don't
283
+ # think the returned message is useful to the agent/learning. Disabling for now.
284
+ await self.exec_tool_calls(
285
+ valid_action, ordered=True, handle_tool_exc=False
286
+ )
287
+ + [
288
+ ToolResponseMessage.from_call(tool_call, content="Invalid tool call.")
289
+ for tool_call in invalid_action.tool_calls
290
+ ],
291
+ )
292
+ return response_messages, self.state.reward, self.state.done, False
293
+
294
+ def export_frame(self) -> Frame:
295
+ """Export the current state of the environment as a Frame object.
296
+
297
+ This method creates and returns a Frame object that captures the current state and additional
298
+ information of the HotPotQA environment. This can be useful for logging, debugging, or visualization
299
+ purposes.
300
+
301
+ Returns:
302
+ Frame: A Frame object containing the current state and additional information of the environment.
303
+
304
+ The returned Frame object includes the following information:
305
+ - state (dict): An empty dictionary representing the current state of the environment.
306
+ - info (dict): A dictionary containing:
307
+ - "steps" (int): The number of steps taken in the current episode.
308
+ - "done" (bool): A flag indicating whether the episode is finished.
309
+ - "reward" (float): The accumulated reward in the current episode.
310
+ - "answer" (Optional[str]): The answer provided by the agent, if any.
311
+
312
+ Example:
313
+ >>> env = HotPotQAEnv()
314
+ >>> frame = env.export_frame()
315
+ >>> print(frame.info)
316
+ {'steps': 0, 'done': False, 'reward': 0.0, 'answer': None}
317
+ """
318
+ return Frame(
319
+ info={
320
+ "steps": self.state.steps,
321
+ "done": self.state.done,
322
+ "reward": self.state.reward,
323
+ "answer": self.state.answer,
324
+ }
325
+ )
326
+
327
+ def finish(self, answer: str) -> str:
328
+ """Finish the episode.
329
+
330
+ Args:
331
+ answer: The answer to the question.
332
+ """
333
+ self.state.done = True
334
+ if not answer:
335
+ return "Finish failed. No answer provided."
336
+
337
+ self.state.answer = answer
338
+ self.state.reward += self.calculate_reward(answer)
339
+ return "Finished."
340
+
341
+ async def search(self, entity: str) -> str:
342
+ """Searches Wikipedia for the given entity and processes the results.
343
+
344
+ Args:
345
+ entity: The entity to search for on Wikipedia.
346
+
347
+ Functionality:
348
+ - Constructs and sends a search query to Wikipedia.
349
+ - Parses the search results.
350
+ - If similar results are found, returns a list of similar titles.
351
+ - If the entity page is found, returns a summary of the page content.
352
+ - Handles disambiguation pages by recursively searching with modified query.
353
+ """
354
+ if not entity:
355
+ self.state.done = True
356
+ return "Search failed. No entity provided."
357
+
358
+ # In case the searched entity is e.g. a year
359
+ search_entity = (
360
+ str(entity) if isinstance(entity, int) else entity.replace(" ", "+") # type: ignore[redundant-expr,unreachable] # noqa: FURB123
361
+ )
362
+ try:
363
+ # Access from cache if we previously searched for search_entity
364
+ response_text = self.wiki_cache[search_entity]
365
+ except KeyError:
366
+ # follow_redirects=True because wikipedia frequently redirects to the correct page
367
+ async with httpx.AsyncClient(
368
+ follow_redirects=True, proxy=self.proxy
369
+ ) as client:
370
+ response = await fetch_with_retry(
371
+ client,
372
+ f"https://en.wikipedia.org/w/index.php?search={search_entity}",
373
+ timeout=15,
374
+ )
375
+ response.raise_for_status() # Raise an HTTPError for bad responses
376
+ # Cache for subsequent tool calls
377
+ self.wiki_cache[search_entity] = response_text = response.text
378
+ soup = BeautifulSoup(response_text, features="html.parser")
379
+ result_divs = soup.find_all("div", {"class": "mw-search-result-heading"})
380
+ if result_divs: # mismatch
381
+ result_titles = [clean_str(div.get_text().strip()) for div in result_divs]
382
+ self.state.page = None
383
+ return f"Could not find {entity}. Similar: {result_titles[:5]}."
384
+
385
+ page = [p.get_text().strip() for p in soup.find_all("p") + soup.find_all("ul")]
386
+ if any("may refer to:" in p for p in page): # Recurse
387
+ return await self.search(entity="[" + entity + "]")
388
+ # Clean up any unicode
389
+ self.state.page = ""
390
+ for p in page:
391
+ if len(p.split(" ")) > 2: # noqa: PLR2004
392
+ self.state.page += clean_str(p)
393
+ if not p.endswith("\n"):
394
+ self.state.page += "\n"
395
+ # Extract and concatenate the first five sentences from the provided page content.
396
+ obs_list = [
397
+ s.strip() + "."
398
+ for p in self.state.page.split("\n")
399
+ if p.strip()
400
+ for s in p.split(". ")
401
+ if s.strip()
402
+ ]
403
+ return " ".join(obs_list[:5])
404
+
405
+ def construct_lookup_list(self, keyword: str) -> str:
406
+ """Construct a list of sentences from the given page content that contain the specified keyword.
407
+
408
+ Args:
409
+ keyword: The keyword to search for within the sentences.
410
+
411
+ Returns:
412
+ A list of sentences containing the keyword. If `page` is None or empty, an empty list is returned.
413
+
414
+ Example:
415
+ page_content = ("Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series
416
+ The Simpsons voiced by Pamela Hayden and created by Matt Groening. Milhouse is Bart Simpson's best friend in
417
+ Mrs. Krabappel's fourth grade class at Springfield Elementary School. He is an insecure, gullible, and
418
+ less popular child than Bart who is often led into trouble by Bart, who takes advantage of his friend's naivety.
419
+ Milhouse is a regular target for school bully Nelson Muntz and his friends Jimbo Jones, Dolph Starbeam and
420
+ Kearney Zzyzwicz. Milhouse has a crush on Bart's sister, Lisa, a common plot element.")
421
+
422
+ keyword = "Milhouse"
423
+ result = construct_lookup_list_from_page(keyword, page_content)
424
+ # Output: ['Milhouse Mussolini Van Houten is a recurring character in the Fox animated television series The
425
+ Simpsons.',
426
+ 'Milhouse is Bart Simpson's best friend in Mrs. Krabappel's fourth grade class at Springfield Elementary
427
+ School.',
428
+ 'Milhouse is a regular target for school bully Nelson Muntz and his friends Jimbo Jones, Dolph Starbeam and
429
+ Kearney Zzyzwicz.',
430
+ 'Milhouse has a crush on Bart's sister, Lisa, a common plot element.']
431
+
432
+ """
433
+ if not keyword:
434
+ self.state.done = True
435
+ return "Lookup failed. No keyword provided"
436
+
437
+ if not self.state.page:
438
+ return "Lookup failed. You have not specified a Wikipedia page yet."
439
+
440
+ if self.state.last_lookup != keyword:
441
+ self.state.last_lookup = keyword
442
+ self.state.lookup_results = [
443
+ s.strip() + "."
444
+ for s in self.state.page.split(". ")
445
+ if s.strip() and keyword.lower() in s.lower()
446
+ ]
447
+ self.state.lookup_index = 0
448
+
449
+ if self.state.lookup_index >= len(self.state.lookup_results):
450
+ return "No more results."
451
+
452
+ obs = f"(Result {self.state.lookup_index + 1} / {len(self.state.lookup_results)}) {self.state.lookup_results[self.state.lookup_index]}"
453
+ self.state.lookup_index += 1
454
+ return obs
455
+
456
+
457
+ class HotPotQADifficultyLevel(StrEnum):
458
+ EASY = "easy"
459
+ MEDIUM = "medium"
460
+ HARD = "hard"
461
+
462
+
463
+ class HotPotQAEnvConfig(BaseModel):
464
+ """Configuration model for the HotPotQA environment.
465
+
466
+ This defines the configuration parameters for setting up the HotPotQA environment,
467
+ including the path to the data repository and the specific path to the prompt file.
468
+ """
469
+
470
+ model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
471
+
472
+ shuffle_data: bool = Field(
473
+ default=False, description="Set True to shuffle the dataset after loading."
474
+ )
475
+ data_fraction: float = Field(
476
+ default=1.0,
477
+ description=(
478
+ "Fraction of data to use, default is full dataset. If set, data is"
479
+ " subsampled after shuffling (if enabled)."
480
+ ),
481
+ gt=0.0,
482
+ le=1.0,
483
+ )
484
+ correct_reward: float = 1.0
485
+ incorrect_reward: float = 0.0
486
+ tool_failure_reward: float = 0.0
487
+ difficulty_level: set[HotPotQADifficultyLevel] | None = Field(
488
+ None,
489
+ description="The HotPotQA difficulty level to filter on. "
490
+ "Can be any subset of `{'easy', 'medium', 'hard'}`. "
491
+ "If None, we will not filter any task instances. "
492
+ "Note that not all splits have a 'level' field.",
493
+ )
494
+ proxy: str | None = None
495
+
496
+
497
+ class HotPotQADataset(TaskDataset[HotPotQAEnv]):
498
+ # SEE: https://huggingface.co/datasets/hotpotqa/hotpot_qa
499
+ HOTPOTQA_HUGGING_FACE_DATASET = "hotpotqa/hotpot_qa"
500
+
501
+ def get_data_from_hugging_face(
502
+ self, split: str, hf_dataset: str = HOTPOTQA_HUGGING_FACE_DATASET
503
+ ) -> list[tuple[str, str]]:
504
+ """Convert a local file and split to a list of (question, answer) tuples."""
505
+ if split in {"dev", "eval", "val"}: # Map common aliases
506
+ split = "validation"
507
+ all_datasets = load_dataset(hf_dataset, name="fullwiki", trust_remote_code=True)
508
+ try:
509
+ data = all_datasets[split].select_columns(
510
+ column_names=["question", "answer", "level"]
511
+ )
512
+ except KeyError as exc:
513
+ raise ValueError(
514
+ f"Split {split!r} was invalid for Hugging Face dataset {hf_dataset},"
515
+ f" please specify a split from {set(all_datasets.keys())}."
516
+ ) from exc
517
+
518
+ return [(d["question"], d["answer"]) for d in data if self._filter_task(d)]
519
+
520
+ def __init__(
521
+ self, split: str, config: HotPotQAEnvConfig | dict | None = None, **kwargs
522
+ ):
523
+ super().__init__()
524
+ if isinstance(config, dict): # Serialized config
525
+ config = HotPotQAEnvConfig(**(config | kwargs))
526
+ elif config is None:
527
+ config = HotPotQAEnvConfig(**kwargs)
528
+ self.config = config
529
+ raw_data = self.get_data_from_hugging_face(split)
530
+ if self.config.shuffle_data:
531
+ random.shuffle(raw_data)
532
+ if self.config.data_fraction < 1.0:
533
+ raw_data = raw_data[: int(self.config.data_fraction * len(raw_data))]
534
+ self.data = raw_data
535
+
536
+ def _filter_task(self, task: dict[str, Any]) -> bool:
537
+ """Decide whether to keep a task instance based on configuration options."""
538
+ if self.config.difficulty_level is None:
539
+ return True
540
+
541
+ try:
542
+ return task["level"] in self.config.difficulty_level
543
+ except KeyError as e:
544
+ raise RuntimeError(
545
+ "Attempting to filter difficulty level, "
546
+ "but this split does not have a 'level' field."
547
+ ) from e
548
+
549
+ def get_new_env_by_idx(self, idx: int) -> HotPotQAEnv:
550
+ return HotPotQAEnv(
551
+ *self.data[idx],
552
+ correct_reward=self.config.correct_reward,
553
+ incorrect_reward=self.config.incorrect_reward,
554
+ tool_failure_reward=self.config.tool_failure_reward,
555
+ proxy=self.config.proxy,
556
+ )
557
+
558
+ def __len__(self) -> int:
559
+ return len(self.data)
File without changes
@@ -0,0 +1,16 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '0.1.0'
16
+ __version_tuple__ = version_tuple = (0, 1, 0)
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.1
2
+ Name: aviary.hotpotqa
3
+ Version: 0.1.0
4
+ Summary: HotPotQA environment implemented with aviary
5
+ Author-email: FutureHouse technical staff <hello@futurehouse.org>
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: beautifulsoup4
9
+ Requires-Dist: datasets
10
+ Requires-Dist: fhaviary
11
+ Requires-Dist: httpx
12
+ Requires-Dist: pydantic~=2.0
13
+ Requires-Dist: tenacity
14
+
15
+ # aviary.hotpotqa
16
+
17
+ HotPotQA environment implemented with aviary.
18
+
19
+ ## References
20
+
21
+ [1] Yang et al. [HotpotQA: A Dataset for Diverse,
22
+ Explainable Multi-Hop Question Answering](https://aclanthology.org/D18-1259/). EMNLP, 2018.
23
+
24
+ [2] Yao et al.,
25
+ [ReAct: Synergizing Reasoning and Acting in Language Models](https://openreview.net/forum?id=WE_vluYUL-X).
26
+ In The Eleventh International Conference on Learning Representations. 2023
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/aviary.hotpotqa.egg-info/PKG-INFO
4
+ src/aviary.hotpotqa.egg-info/SOURCES.txt
5
+ src/aviary.hotpotqa.egg-info/dependency_links.txt
6
+ src/aviary.hotpotqa.egg-info/requires.txt
7
+ src/aviary.hotpotqa.egg-info/top_level.txt
8
+ src/aviary/hotpotqa/__init__.py
9
+ src/aviary/hotpotqa/env.py
10
+ src/aviary/hotpotqa/py.typed
11
+ src/aviary/hotpotqa/version.py
12
+ tests/test_hotpotqa_env.py
@@ -0,0 +1,6 @@
1
+ beautifulsoup4
2
+ datasets
3
+ fhaviary
4
+ httpx
5
+ pydantic~=2.0
6
+ tenacity
@@ -0,0 +1,22 @@
1
+ from aviary.env import Environment, TaskDataset
2
+ from aviary.hotpotqa import HotPotQAEnv
3
+
4
+
5
+ def test_env_construction() -> None:
6
+ hotpotqa_env: HotPotQAEnv = Environment.from_name(
7
+ "hotpotqa",
8
+ question="What is the formula for the volume of Abraham Lincoln's favorite hat?",
9
+ correct_answer="pi*r^2*h",
10
+ )
11
+ assert isinstance(hotpotqa_env, HotPotQAEnv)
12
+
13
+
14
+ def test_dataset_from_name() -> None:
15
+ dataset = TaskDataset.from_name("hotpotqa", split="dev")
16
+ assert isinstance(dataset.get_new_env_by_idx(0), HotPotQAEnv)
17
+
18
+ # double-check we can load by difficulty level
19
+ dataset = TaskDataset.from_name(
20
+ "hotpotqa", split="train", difficulty_level={"easy", "hard"}
21
+ )
22
+ assert len(dataset) == 33633