nora-lib 0.0.5.dev2__tar.gz → 0.0.6.dev0__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 (25) hide show
  1. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/PKG-INFO +2 -1
  2. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/interactions/interactions_service.py +76 -10
  3. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/interactions/models.py +8 -6
  4. nora_lib-0.0.6.dev0/nora_lib/tasks/state.py +163 -0
  5. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib.egg-info/PKG-INFO +2 -1
  6. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib.egg-info/requires.txt +1 -0
  7. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/setup.py +2 -2
  8. nora_lib-0.0.5.dev2/nora_lib/tasks/state.py +0 -53
  9. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/README.md +0 -0
  10. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/__init__.py +0 -0
  11. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/context/__init__.py +0 -0
  12. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/context/context_service.py +0 -0
  13. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/context/models.py +0 -0
  14. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/interactions/__init__.py +0 -0
  15. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/py.typed +0 -0
  16. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/tasks/__init__.py +0 -0
  17. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/tasks/models.py +0 -0
  18. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib.egg-info/SOURCES.txt +0 -0
  19. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib.egg-info/dependency_links.txt +0 -0
  20. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib.egg-info/top_level.txt +0 -0
  21. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/pyproject.toml +0 -0
  22. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/setup.cfg +0 -0
  23. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/tests/tasks/__init__.py +0 -0
  24. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/tests/tasks/test_state.py +0 -0
  25. {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/tests/test_placeholder.py +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nora_lib
3
- Version: 0.0.5.dev2
3
+ Version: 0.0.6.dev0
4
4
  Summary: For making and coordinating agents and tools
5
5
  Home-page: https://github.com/allenai/nora_lib
6
6
  Requires-Python: >=3.9
7
7
  Requires-Dist: pydantic<3,>=2
8
8
  Requires-Dist: requests
9
+ Requires-Dist: boto3
9
10
  Provides-Extra: dev
10
11
  Requires-Dist: mypy; extra == "dev"
11
12
  Requires-Dist: pytest; extra == "dev"
@@ -2,6 +2,7 @@ from datetime import datetime
2
2
  import logging
3
3
  import requests
4
4
  from typing import List, Optional
5
+ import json
5
6
 
6
7
  from nora_lib.interactions.models import (
7
8
  AnnotationBatch,
@@ -31,7 +32,7 @@ class InteractionsService:
31
32
  message_url,
32
33
  json=message.model_dump(),
33
34
  headers=self.headers,
34
- timeout=int(self.timeout),
35
+ timeout=self.timeout,
35
36
  )
36
37
  response.raise_for_status()
37
38
 
@@ -42,7 +43,7 @@ class InteractionsService:
42
43
  event_url,
43
44
  json=event.model_dump(),
44
45
  headers=self.headers,
45
- timeout=int(self.timeout),
46
+ timeout=self.timeout,
46
47
  )
47
48
  response.raise_for_status()
48
49
 
@@ -53,7 +54,7 @@ class InteractionsService:
53
54
  annotation_url,
54
55
  json=annotation.model_dump(),
55
56
  headers=self.headers,
56
- timeout=int(self.timeout),
57
+ timeout=self.timeout,
57
58
  )
58
59
  response.raise_for_status()
59
60
 
@@ -68,7 +69,7 @@ class InteractionsService:
68
69
  message_url,
69
70
  json=request_body,
70
71
  headers=self.headers,
71
- timeout=int(self.timeout),
72
+ timeout=self.timeout,
72
73
  )
73
74
  response.raise_for_status()
74
75
  res_dict = response.json()["message"]
@@ -92,7 +93,7 @@ class InteractionsService:
92
93
  message_url,
93
94
  json=request_body,
94
95
  headers=self.headers,
95
- timeout=int(self.timeout),
96
+ timeout=self.timeout,
96
97
  )
97
98
  response.raise_for_status()
98
99
  return response.json()
@@ -114,7 +115,8 @@ class InteractionsService:
114
115
  # Process any thread_fork events
115
116
  try:
116
117
  for msg in messages_for_thread.messages:
117
- for event in msg.events:
118
+ events = msg.events if msg.events else []
119
+ for event in events:
118
120
  if event.type == EventType.THREAD_FORK.value:
119
121
  event_data = ThreadForkEventData.model_validate(event.data)
120
122
  forked_thread: ThreadRelationsResponse = (
@@ -130,7 +132,7 @@ class InteractionsService:
130
132
  e,
131
133
  )
132
134
 
133
- messages_with_events.sort(key=lambda x: datetime.fromisoformat(x.ts))
135
+ messages_with_events.sort(key=lambda x: x.ts)
134
136
  return messages_with_events
135
137
 
136
138
  def fetch_thread_messages_and_events_for_message(
@@ -143,7 +145,7 @@ class InteractionsService:
143
145
  message_url,
144
146
  json=request_body,
145
147
  headers=self.headers,
146
- timeout=int(self.timeout),
148
+ timeout=self.timeout,
147
149
  )
148
150
  response.raise_for_status()
149
151
  json_response = response.json()
@@ -176,11 +178,46 @@ class InteractionsService:
176
178
  thread_search_url,
177
179
  json=request_body,
178
180
  headers=self.headers,
179
- timeout=int(self.timeout),
181
+ timeout=self.timeout,
180
182
  )
181
183
  response.raise_for_status()
182
184
  return response.json()
183
185
 
186
+ def fetch_messages_and_events_for_forked_thread(
187
+ self, message_id: str, event_type: str
188
+ ) -> List[ReturnedMessage]:
189
+ """Build a history of messages for a given message including associated events.
190
+ This includes messages from pre-forked threads."""
191
+ returned_messages: List[ReturnedMessage] = []
192
+
193
+ messages_for_thread: ThreadRelationsResponse = (
194
+ self.fetch_thread_messages_and_events_for_message(message_id, [event_type])
195
+ )
196
+ if messages_for_thread.messages:
197
+ returned_messages.extend(messages_for_thread.messages)
198
+
199
+ # Lookup any thread_fork events (conversation across surfaces)
200
+ thread_fork_events = self.fetch_messages_and_events_for_thread(
201
+ messages_for_thread.thread_id, EventType.THREAD_FORK.value
202
+ )
203
+ for forked_thread_event in thread_fork_events.get("thread", {}).get(
204
+ "events", []
205
+ ):
206
+ event_data = ThreadForkEventData.model_validate(
207
+ forked_thread_event.get("data", {})
208
+ )
209
+ forked_thread: ThreadRelationsResponse = (
210
+ self.fetch_thread_messages_and_events_for_message(
211
+ event_data.previous_message_id, [event_type]
212
+ )
213
+ )
214
+ if forked_thread.messages:
215
+ returned_messages.extend(forked_thread.messages)
216
+
217
+ returned_messages.sort(key=lambda x: x.ts)
218
+
219
+ return returned_messages
220
+
184
221
  def fetch_events_for_message(
185
222
  self,
186
223
  message_id: str,
@@ -199,7 +236,7 @@ class InteractionsService:
199
236
  message_search_url,
200
237
  json=request_body,
201
238
  headers=self.headers,
202
- timeout=int(self.timeout),
239
+ timeout=self.timeout,
203
240
  )
204
241
  response.raise_for_status()
205
242
  return response.json()
@@ -237,3 +274,32 @@ class InteractionsService:
237
274
  }
238
275
  },
239
276
  }
277
+
278
+ @staticmethod
279
+ def prod() -> "InteractionsService":
280
+ return InteractionsService(
281
+ base_url="https://s2ub.prod.s2.allenai.org/service/noraretrieval",
282
+ timeout=30,
283
+ token=InteractionsService._fetch_bearer_token(
284
+ "nora/prod/interaction-bearer-token"
285
+ ),
286
+ )
287
+
288
+ @staticmethod
289
+ def dev() -> "InteractionsService":
290
+ return InteractionsService(
291
+ base_url="https://s2ub.dev.s2.allenai.org/service/noraretrieval",
292
+ timeout=30,
293
+ token=InteractionsService._fetch_bearer_token(
294
+ "nora/dev/interaction-bearer-token"
295
+ ),
296
+ )
297
+
298
+ @staticmethod
299
+ def _fetch_bearer_token(secret_id: str) -> str:
300
+ import boto3
301
+
302
+ secrets_manager = boto3.client("secretsmanager", region_name="us-west-2")
303
+ return json.loads(
304
+ secrets_manager.get_secret_value(SecretId=secret_id)["SecretString"]
305
+ )["token"]
@@ -58,7 +58,9 @@ class EventType(str, Enum):
58
58
  """Event types for the interactions service"""
59
59
 
60
60
  AGENT_CONTEXT = "agent:message_context"
61
+ S2_ANNOTATION = "s2_annotation"
61
62
  THREAD_FORK = "thread_fork"
63
+ UI_INTERACTION = "ui_interaction"
62
64
 
63
65
 
64
66
  class Event(BaseModel):
@@ -88,11 +90,11 @@ class ReturnedMessage(BaseModel):
88
90
  """Message format returned by interaction service"""
89
91
 
90
92
  message_id: str
91
- actor_id: str
93
+ actor_id: UUID
92
94
  text: str
93
- ts: str
95
+ ts: datetime
94
96
  annotated_text: Optional[str] = None
95
- events: List[Event] = Field(default_factory=list)
97
+ events: Optional[List[Event]] = None
96
98
  thread_id: Optional[str] = None
97
99
  channel_id: Optional[str] = None
98
100
  annotations: Optional[List[Annotation]] = None
@@ -111,8 +113,8 @@ class AgentMessageData(BaseModel):
111
113
  class ReturnedAgentContextEvent(BaseModel):
112
114
  """Event format returned by interaction service for agent context events"""
113
115
 
114
- actor_id: str # agent that saved this context
115
- timestamp: str
116
+ actor_id: UUID # agent that saved this context
117
+ timestamp: datetime
116
118
  data: AgentMessageData
117
119
  type: str
118
120
 
@@ -121,7 +123,7 @@ class ReturnedAgentContextMessage(BaseModel):
121
123
  """Message format returned by interaction service for search by thread"""
122
124
 
123
125
  message_id: str
124
- actor_id: str
126
+ actor_id: UUID
125
127
  text: str
126
128
  ts: str
127
129
  annotated_text: Optional[str] = None
@@ -0,0 +1,163 @@
1
+ """
2
+ Handles IO for asynchronous task-related state.
3
+
4
+ A StateManager instance must be initialized with
5
+ a concrete subclass of `AsyncTaskState`, as implemented
6
+ by dependent projects.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ from uuid import UUID
12
+ from datetime import datetime, timezone
13
+ from typing import Generic, Optional, Type, Any
14
+ from abc import ABC, abstractmethod
15
+
16
+ from nora_lib.tasks.models import AsyncTaskState, R, TASK_STATUSES
17
+ from nora_lib.interactions.interactions_service import InteractionsService
18
+ from nora_lib.interactions.models import Event
19
+
20
+
21
+ class NoSuchTaskException(Exception):
22
+ def __init__(self, task_id: str):
23
+ self._task_id = task_id
24
+
25
+ def __str__(self):
26
+ return f"No record found for task {self._task_id}"
27
+
28
+
29
+ class IStateManager(ABC, Generic[R]):
30
+ @abstractmethod
31
+ def read_state(self, task_id: str) -> AsyncTaskState[R]:
32
+ pass
33
+
34
+ @abstractmethod
35
+ def write_state(self, state: AsyncTaskState[R]) -> None:
36
+ pass
37
+
38
+ def update_status(self, task_id: str, new_status: str) -> None:
39
+ state = self.read_state(task_id)
40
+ state.task_status = new_status
41
+ self.write_state(state)
42
+
43
+ def save_result(self, task_id: str, task_result: R) -> None:
44
+ state = self.read_state(task_id)
45
+ state.task_status = TASK_STATUSES["COMPLETED"]
46
+ state.task_result = task_result
47
+ self.write_state(state)
48
+
49
+
50
+ class StateManager(IStateManager[R]):
51
+ """
52
+ Stores task state on local disk
53
+ """
54
+
55
+ def __init__(self, task_state_class: Type[AsyncTaskState[R]], state_dir) -> None:
56
+ self._task_state_class = task_state_class
57
+ self._state_dir = state_dir
58
+
59
+ def read_state(self, task_id: str) -> AsyncTaskState[R]:
60
+ task_state_path = os.path.join(self._state_dir, f"{task_id}.json")
61
+ if not os.path.isfile(task_state_path):
62
+ raise NoSuchTaskException(task_id)
63
+
64
+ with open(task_state_path, "r") as f:
65
+ return self._task_state_class(**json.loads(f.read()))
66
+
67
+ def write_state(self, state: AsyncTaskState[R]) -> None:
68
+ task_state_path = os.path.join(self._state_dir, f"{state.task_id}.json")
69
+ with open(task_state_path, "w") as f:
70
+ json.dump(state.model_dump(), f)
71
+
72
+
73
+ class TaskStateFetchException(Exception):
74
+ def __init__(self, message: str):
75
+ super().__init__(message)
76
+
77
+
78
+ class RemoteStateManagerFactory:
79
+ """
80
+ Stores task state in the interaction store
81
+ """
82
+
83
+ def __init__(
84
+ self, agent_name: str, actor_id: UUID, interactions_service: InteractionsService
85
+ ):
86
+ """
87
+ :param agent_name: Used to form the event type that will hold the task state in the interactions store
88
+ :param actor_id: Associated with the events written to the interactions store
89
+ :param interactions_service:
90
+ """
91
+ self.agent_name = agent_name
92
+ self.actor_id = actor_id
93
+ self.interactions_service = interactions_service
94
+
95
+ def for_message(self, message_id: str) -> IStateManager[R]:
96
+ return RemoteStateManager(
97
+ self.agent_name, self.actor_id, self.interactions_service, message_id
98
+ )
99
+
100
+
101
+ class RemoteStateManager(IStateManager[R]):
102
+ """
103
+ Stores task state in the interaction store
104
+ """
105
+
106
+ _TASK_STATE_EVENT_TYPE = "agent:{}:task_state"
107
+
108
+ def __init__(
109
+ self,
110
+ agent_name: str,
111
+ actor_id: UUID,
112
+ interactions_service: InteractionsService,
113
+ message_id: str,
114
+ ):
115
+ """
116
+ :param agent_name: Agent that saved the task
117
+ :param actor_id: ID for the agent (ignored when reading)
118
+ :param message_id: The message that initiated the request for task status
119
+ """
120
+ self.agent_name = agent_name
121
+ self.actor_id = actor_id
122
+ self.message_id = message_id
123
+ self.interactions_service = interactions_service
124
+
125
+ def read_state(self, task_id: str) -> AsyncTaskState[R]:
126
+ event_type = RemoteStateManager._TASK_STATE_EVENT_TYPE.format(self.agent_name)
127
+ response = (
128
+ self.interactions_service.fetch_thread_messages_and_events_for_message(
129
+ self.message_id, [event_type]
130
+ )
131
+ )
132
+ latest_state: Optional[AsyncTaskState[R]] = None
133
+ latest_timestamp = None
134
+ for msg in response.messages or []:
135
+ for event in msg.events or []:
136
+ try:
137
+ state = AsyncTaskState[Any].model_validate(event.data)
138
+ except Exception as e:
139
+ # Event json blob has unexpected format
140
+ raise TaskStateFetchException(
141
+ f"Event of type {event_type} for message {self.message_id} does not deserialize to AsyncTaskState: {e}"
142
+ )
143
+ if state.task_id != task_id:
144
+ continue
145
+ if latest_state is None or (
146
+ latest_timestamp and event.timestamp > latest_timestamp
147
+ ):
148
+ latest_state = state
149
+ latest_timestamp = event.timestamp
150
+
151
+ if not latest_state:
152
+ raise NoSuchTaskException(task_id)
153
+ return latest_state
154
+
155
+ def write_state(self, state: AsyncTaskState[R]) -> None:
156
+ event = Event(
157
+ type=RemoteStateManager._TASK_STATE_EVENT_TYPE.format(self.agent_name),
158
+ actor_id=self.actor_id,
159
+ timestamp=datetime.now(tz=timezone.utc),
160
+ message_id=self.message_id,
161
+ data=state.model_dump(),
162
+ )
163
+ self.interactions_service.save_event(event)
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nora_lib
3
- Version: 0.0.5.dev2
3
+ Version: 0.0.6.dev0
4
4
  Summary: For making and coordinating agents and tools
5
5
  Home-page: https://github.com/allenai/nora_lib
6
6
  Requires-Python: >=3.9
7
7
  Requires-Dist: pydantic<3,>=2
8
8
  Requires-Dist: requests
9
+ Requires-Dist: boto3
9
10
  Provides-Extra: dev
10
11
  Requires-Dist: mypy; extra == "dev"
11
12
  Requires-Dist: pytest; extra == "dev"
@@ -1,5 +1,6 @@
1
1
  pydantic<3,>=2
2
2
  requests
3
+ boto3
3
4
 
4
5
  [dev]
5
6
  mypy
@@ -1,13 +1,13 @@
1
1
  import setuptools
2
2
 
3
- runtime_requirements = ["pydantic>=2,<3", "requests"]
3
+ runtime_requirements = ["pydantic>=2,<3", "requests", "boto3"]
4
4
 
5
5
  # For running tests, linting, etc
6
6
  dev_requirements = ["mypy", "pytest", "black", "types-requests"]
7
7
 
8
8
  setuptools.setup(
9
9
  name="nora_lib",
10
- version="0.0.5.dev2",
10
+ version="0.0.6.dev0",
11
11
  description="For making and coordinating agents and tools",
12
12
  url="https://github.com/allenai/nora_lib",
13
13
  packages=setuptools.find_packages(exclude=(["tests"])),
@@ -1,53 +0,0 @@
1
- """
2
- Handles IO for asynchronous task-related state.
3
- Currently just reads/writes from local disk,
4
- the best and most robust mechanism.
5
-
6
- A StateManager instance must be initialized with
7
- a concrete subclass of `AsyncTaskState`, as implemented
8
- by dependent projects.
9
- """
10
-
11
- import json
12
- import os
13
- from typing import Generic, Optional, Type
14
-
15
- from nora_lib.tasks.models import AsyncTaskState, R, TASK_STATUSES
16
-
17
-
18
- class NoSuchTaskException(Exception):
19
- def __init__(self, task_id: str):
20
- self._task_id = task_id
21
-
22
- def __str__(self):
23
- return f"No record found for task {self._task_id}"
24
-
25
-
26
- class StateManager(Generic[R]):
27
- def __init__(self, task_state_class: Type[AsyncTaskState[R]], state_dir) -> None:
28
- self._task_state_class = task_state_class
29
- self._state_dir = state_dir
30
-
31
- def read_state(self, task_id: str) -> AsyncTaskState[R]:
32
- task_state_path = os.path.join(self._state_dir, f"{task_id}.json")
33
- if not os.path.isfile(task_state_path):
34
- raise NoSuchTaskException(task_id)
35
-
36
- with open(task_state_path, "r") as f:
37
- return self._task_state_class(**json.loads(f.read()))
38
-
39
- def write_state(self, state: AsyncTaskState[R]) -> None:
40
- task_state_path = os.path.join(self._state_dir, f"{state.task_id}.json")
41
- with open(task_state_path, "w") as f:
42
- json.dump(state.model_dump(), f)
43
-
44
- def update_status(self, task_id: str, new_status: str) -> None:
45
- state = self.read_state(task_id)
46
- state.task_status = new_status
47
- self.write_state(state)
48
-
49
- def save_result(self, task_id: str, task_result: R) -> None:
50
- state = self.read_state(task_id)
51
- state.task_status = TASK_STATUSES["COMPLETED"]
52
- state.task_result = task_result
53
- self.write_state(state)
File without changes
File without changes