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.
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/PKG-INFO +2 -1
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/interactions/interactions_service.py +76 -10
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/interactions/models.py +8 -6
- nora_lib-0.0.6.dev0/nora_lib/tasks/state.py +163 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib.egg-info/PKG-INFO +2 -1
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib.egg-info/requires.txt +1 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/setup.py +2 -2
- nora_lib-0.0.5.dev2/nora_lib/tasks/state.py +0 -53
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/README.md +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/__init__.py +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/context/__init__.py +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/context/context_service.py +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/context/models.py +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/interactions/__init__.py +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/py.typed +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/tasks/__init__.py +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib/tasks/models.py +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib.egg-info/SOURCES.txt +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib.egg-info/dependency_links.txt +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/nora_lib.egg-info/top_level.txt +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/pyproject.toml +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/setup.cfg +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/tests/tasks/__init__.py +0 -0
- {nora_lib-0.0.5.dev2 → nora_lib-0.0.6.dev0}/tests/tasks/test_state.py +0 -0
- {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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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:
|
|
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=
|
|
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=
|
|
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=
|
|
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:
|
|
93
|
+
actor_id: UUID
|
|
92
94
|
text: str
|
|
93
|
-
ts:
|
|
95
|
+
ts: datetime
|
|
94
96
|
annotated_text: Optional[str] = None
|
|
95
|
-
events: List[Event] =
|
|
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:
|
|
115
|
-
timestamp:
|
|
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:
|
|
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.
|
|
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,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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|