nora-lib 0.0.4__tar.gz → 0.0.5.dev1__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 (27) hide show
  1. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/PKG-INFO +1 -1
  2. nora_lib-0.0.5.dev1/nora_lib/context/context_service.py +42 -0
  3. nora_lib-0.0.5.dev1/nora_lib/context/models.py +11 -0
  4. nora_lib-0.0.5.dev1/nora_lib/interactions/interactions_service.py +227 -0
  5. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/nora_lib/interactions/models.py +45 -6
  6. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/nora_lib.egg-info/PKG-INFO +1 -1
  7. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/setup.py +1 -1
  8. nora_lib-0.0.4/nora_lib/context/context_service.py +0 -95
  9. nora_lib-0.0.4/nora_lib/context/models.py +0 -21
  10. nora_lib-0.0.4/nora_lib/interactions/interactions_service.py +0 -134
  11. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/README.md +0 -0
  12. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/nora_lib/__init__.py +0 -0
  13. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/nora_lib/context/__init__.py +0 -0
  14. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/nora_lib/interactions/__init__.py +0 -0
  15. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/nora_lib/py.typed +0 -0
  16. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/nora_lib/tasks/__init__.py +0 -0
  17. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/nora_lib/tasks/models.py +0 -0
  18. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/nora_lib/tasks/state.py +0 -0
  19. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/nora_lib.egg-info/SOURCES.txt +0 -0
  20. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/nora_lib.egg-info/dependency_links.txt +0 -0
  21. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/nora_lib.egg-info/requires.txt +0 -0
  22. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/nora_lib.egg-info/top_level.txt +0 -0
  23. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/pyproject.toml +0 -0
  24. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/setup.cfg +0 -0
  25. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/tests/tasks/__init__.py +0 -0
  26. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/tests/tasks/test_state.py +0 -0
  27. {nora_lib-0.0.4 → nora_lib-0.0.5.dev1}/tests/test_placeholder.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nora_lib
3
- Version: 0.0.4
3
+ Version: 0.0.5.dev1
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
@@ -0,0 +1,42 @@
1
+ from datetime import datetime, timezone
2
+ from typing import List, Optional
3
+ from uuid import UUID
4
+
5
+ from nora_lib.interactions.interactions_service import InteractionsService
6
+ from nora_lib.interactions.models import (
7
+ ReturnedMessage,
8
+ ReturnedAgentContextMessage,
9
+ ReturnedAgentContextEvent,
10
+ EventType,
11
+ AgentMessageData,
12
+ Event,
13
+ )
14
+ from nora_lib.context.models import WrappedTaskObject
15
+
16
+
17
+ class ContextService:
18
+ """
19
+ Save and retrieve task agent context from interaction store
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ agent_actor_id: str, # uuid representing this agent in interaction store
25
+ interactions_base_url: str,
26
+ interactions_bearer_token: Optional[str],
27
+ timeout: int = 30,
28
+ ):
29
+ self.interactions_service = self._get_interactions_service(
30
+ interactions_base_url, interactions_bearer_token, timeout
31
+ )
32
+ self.agent_actor_id = agent_actor_id
33
+
34
+ def _get_interactions_service(self, url, token, timeout) -> InteractionsService:
35
+ return InteractionsService(url, timeout, token)
36
+
37
+ def get_message(self, message_id: str) -> str:
38
+ message: ReturnedMessage = self.interactions_service.get_message(message_id)
39
+ if message.annotated_text:
40
+ return message.annotated_text
41
+ else:
42
+ return message.text
@@ -0,0 +1,11 @@
1
+ from typing import Optional
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class WrappedTaskObject(BaseModel):
6
+ """Encloses request or response object with additional metadata"""
7
+
8
+ message_id: str = Field(
9
+ description="id of originating message; key for istore retrieval"
10
+ )
11
+ data: dict = Field(description="Tool-defined request or response")
@@ -0,0 +1,227 @@
1
+ from datetime import datetime
2
+ import logging
3
+ import requests
4
+ from typing import List, Optional
5
+
6
+ from nora_lib.interactions.models import (
7
+ Event,
8
+ EventType,
9
+ Message,
10
+ ReturnedMessage,
11
+ ThreadRelationsResponse,
12
+ ThreadForkEventData,
13
+ )
14
+
15
+
16
+ class InteractionsService:
17
+ """
18
+ Service which saves interactions to the Interactions API
19
+ """
20
+
21
+ def __init__(self, base_url: str, timeout: int = 30, token: Optional[str] = None):
22
+ self.base_url = base_url
23
+ self.timeout = timeout
24
+ self.headers = {"Authorization": f"Bearer {token}"} if token else None
25
+
26
+ def save_message(self, message: Message) -> None:
27
+ """Save a message to the Interactions API"""
28
+ message_url = f"{self.base_url}/interaction/v1/message"
29
+ response = requests.post(
30
+ message_url,
31
+ json=message.model_dump(),
32
+ headers=self.headers,
33
+ timeout=int(self.timeout),
34
+ )
35
+ response.raise_for_status()
36
+
37
+ def save_event(self, event: Event) -> None:
38
+ """Save an event to the Interactions API"""
39
+ event_url = f"{self.base_url}/interaction/v1/event"
40
+ response = requests.post(
41
+ event_url,
42
+ json=event.model_dump(),
43
+ headers=self.headers,
44
+ timeout=int(self.timeout),
45
+ )
46
+ response.raise_for_status()
47
+
48
+ def get_message(self, message_id: str) -> ReturnedMessage:
49
+ """Fetch a message from the Interactions API"""
50
+ message_url = f"{self.base_url}/interaction/v1/search/message"
51
+ request_body = {
52
+ "id": message_id,
53
+ "relations": {"thread": {}, "channel": {}, "events": {}, "annotations": {}},
54
+ }
55
+ response = requests.post(
56
+ message_url,
57
+ json=request_body,
58
+ headers=self.headers,
59
+ timeout=int(self.timeout),
60
+ )
61
+ response.raise_for_status()
62
+ res_dict = response.json()["message"]
63
+ res = ReturnedMessage.model_validate(res_dict)
64
+
65
+ # thread_id and channel_id are for some reason nested in the response
66
+ if not res.thread_id:
67
+ res.thread_id = res_dict.get("thread", {}).get("thread_id")
68
+ if not res.channel_id:
69
+ res.channel_id = res_dict.get("channel", {}).get("channel_id")
70
+
71
+ return res
72
+
73
+ def fetch_all_threads_by_channel(self, channel_id: str, min_timestamp: str) -> dict:
74
+ """Fetch a message from the Interactions API"""
75
+ message_url = f"{self.base_url}/interaction/v1/search/channel"
76
+ request_body = self._channel_lookup_request(
77
+ channel_id=channel_id, min_timestamp=min_timestamp
78
+ )
79
+ response = requests.post(
80
+ message_url,
81
+ json=request_body,
82
+ headers=self.headers,
83
+ timeout=int(self.timeout),
84
+ )
85
+ response.raise_for_status()
86
+ return response.json()
87
+
88
+ def fetch_messages_and_agent_context_events_for_thread(
89
+ self, message_id: str, event_type: str
90
+ ) -> List[ReturnedMessage]:
91
+ """Build a history of messages for a given message including associated events.
92
+ This includes messages from pre-forked threads."""
93
+ messages_with_events: List[ReturnedMessage] = []
94
+
95
+ messages_for_thread: ThreadRelationsResponse = (
96
+ self.fetch_thread_messages_and_events_for_message(
97
+ message_id, [event_type, EventType.THREAD_FORK.value]
98
+ )
99
+ )
100
+ messages_with_events.extend(messages_for_thread.messages)
101
+
102
+ # Process any thread_fork events
103
+ try:
104
+ for msg in messages_for_thread.messages:
105
+ for event in msg.events:
106
+ if event.type == EventType.THREAD_FORK.value:
107
+ event_data = ThreadForkEventData.model_validate(event.data)
108
+ forked_thread: ThreadRelationsResponse = (
109
+ self.fetch_thread_messages_and_events_for_message(
110
+ event_data.previous_message_id, [event_type]
111
+ )
112
+ )
113
+ messages_with_events.extend(forked_thread.messages)
114
+ except Exception as e: # pylint: disable=broad-except
115
+ logging.exception(
116
+ "Failed to fetch forked thread messages for message %s: %s",
117
+ message_id,
118
+ e,
119
+ )
120
+
121
+ messages_with_events.sort(key=lambda x: datetime.fromisoformat(x.ts))
122
+ return messages_with_events
123
+
124
+ def fetch_thread_messages_and_events_for_message(
125
+ self, message_id: str, event_types: list[str]
126
+ ) -> ThreadRelationsResponse:
127
+ """Fetch messages sorted by timestamp and events for agent context"""
128
+ message_url = f"{self.base_url}/interaction/v1/search/message"
129
+ request_body = self._thread_lookup_request(message_id, event_types=event_types)
130
+ response = requests.post(
131
+ message_url,
132
+ json=request_body,
133
+ headers=self.headers,
134
+ timeout=int(self.timeout),
135
+ )
136
+ response.raise_for_status()
137
+ json_response = response.json()
138
+
139
+ return ThreadRelationsResponse.model_validate(
140
+ json_response.get("message", {}).get("thread", {})
141
+ )
142
+
143
+ def fetch_messages_and_events_for_thread(
144
+ self,
145
+ thread_id: str,
146
+ event_type: Optional[str] = None,
147
+ min_timestamp: Optional[str] = None,
148
+ ) -> dict:
149
+ """Fetch messages and events for the given thread from the Interactions API"""
150
+ thread_search_url = f"{self.base_url}/interaction/v1/search/thread"
151
+ request_body = {
152
+ "id": thread_id,
153
+ "relations": {
154
+ "messages": (
155
+ {"filter": {"min_timestamp": min_timestamp}}
156
+ if min_timestamp
157
+ else {}
158
+ ),
159
+ "events": {"filter": {"type": event_type}} if event_type else {},
160
+ },
161
+ }
162
+
163
+ response = requests.post(
164
+ thread_search_url,
165
+ json=request_body,
166
+ headers=self.headers,
167
+ timeout=int(self.timeout),
168
+ )
169
+ response.raise_for_status()
170
+ return response.json()
171
+
172
+ def fetch_events_for_message(
173
+ self,
174
+ message_id: str,
175
+ event_type: Optional[str] = None,
176
+ ) -> dict:
177
+ """Fetch messages and events for the thread containing a given message from the Interactions API"""
178
+ message_search_url = f"{self.base_url}/interaction/v1/search/message"
179
+ request_body = {
180
+ "id": message_id,
181
+ "relations": {
182
+ "events": {"filter": {"type": event_type}} if event_type else {},
183
+ },
184
+ }
185
+
186
+ response = requests.post(
187
+ message_search_url,
188
+ json=request_body,
189
+ headers=self.headers,
190
+ timeout=int(self.timeout),
191
+ )
192
+ response.raise_for_status()
193
+ return response.json()
194
+
195
+ @staticmethod
196
+ def _channel_lookup_request(channel_id: str, min_timestamp: str) -> dict:
197
+ """Interaction service API request to get threads and messages for a channel"""
198
+ return {
199
+ "id": channel_id,
200
+ "relations": {
201
+ "threads": {
202
+ "relations": {
203
+ "messages": {
204
+ "filter": {"min_timestamp": min_timestamp},
205
+ "apply_annotations_from_actors": ["*"],
206
+ }
207
+ }
208
+ }
209
+ },
210
+ }
211
+
212
+ @staticmethod
213
+ def _thread_lookup_request(message_id: str, event_types: list[str]) -> dict:
214
+ """will return all messages for the thread containing the given message and events associated with each message"""
215
+ return {
216
+ "id": message_id,
217
+ "relations": {
218
+ "thread": {
219
+ "relations": {
220
+ "messages": {
221
+ "relations": {"events": {"filter": {"type": event_types}}},
222
+ "apply_annotations_from_actors": ["*"],
223
+ },
224
+ }
225
+ }
226
+ },
227
+ }
@@ -10,6 +10,40 @@ from uuid import UUID
10
10
  from pydantic import BaseModel, Field, field_serializer, ConfigDict
11
11
 
12
12
 
13
+ class Surface(str, Enum):
14
+ SLACK = "Slack"
15
+ WEB = "NoraWebapp"
16
+
17
+
18
+ class Annotation(BaseModel):
19
+ # Need this config to stringify numeric values in attributes.
20
+ # Otherwise, we'll get 'Input should be a valid string' error.
21
+ model_config = ConfigDict(coerce_numbers_to_str=True)
22
+
23
+ tag: str
24
+ span: Tuple[int, int]
25
+ attributes: Optional[Dict[str, str]] = None
26
+
27
+
28
+ class Message(BaseModel):
29
+ message_id: str
30
+ actor_id: UUID
31
+ text: str
32
+ thread_id: Optional[str]
33
+ channel_id: str
34
+ surface: Surface
35
+ ts: datetime
36
+ annotations: Optional[List[Annotation]] = None
37
+
38
+ @field_serializer("actor_id")
39
+ def serialize_actor_id(self, actor_id: UUID):
40
+ return str(actor_id)
41
+
42
+ @field_serializer("ts")
43
+ def serialize_ts(self, ts: datetime):
44
+ return ts.isoformat()
45
+
46
+
13
47
  class EventType(str, Enum):
14
48
  """Event types for the interactions service"""
15
49
 
@@ -26,7 +60,7 @@ class Event(BaseModel):
26
60
  )
27
61
  timestamp: datetime
28
62
  text: Optional[str] = None
29
- data: Optional[dict] = Field(default_factory=dict)
63
+ data: dict = Field(default_factory=dict)
30
64
  message_id: Optional[str] = None
31
65
  thread_id: Optional[str] = None
32
66
  channel_id: Optional[str] = None
@@ -48,7 +82,10 @@ class ReturnedMessage(BaseModel):
48
82
  text: str
49
83
  ts: str
50
84
  annotated_text: Optional[str] = None
51
- events: Optional[List[dict]] = None
85
+ events: List[Event] = Field(default_factory=list)
86
+ thread_id: Optional[str] = None
87
+ channel_id: Optional[str] = None
88
+ annotations: Optional[List[Annotation]] = None
52
89
 
53
90
 
54
91
  class AgentMessageData(BaseModel):
@@ -91,10 +128,12 @@ class ThreadRelationsResponse(BaseModel):
91
128
  """Thread format returned by interaction service for thread relations in a search response"""
92
129
 
93
130
  thread_id: str
94
- events: Optional[List[Event]] = None # events associated only with the thread
95
- messages: Optional[List[ReturnedMessage]] = (
96
- None # includes events associated with each message
97
- )
131
+ events: List[Event] = Field(
132
+ default_factory=list
133
+ ) # events associated only with the thread
134
+ messages: List[ReturnedMessage] = Field(
135
+ default_factory=list
136
+ ) # includes events associated with each message
98
137
 
99
138
 
100
139
  def thread_message_lookup_request(message_id: str, event_type: str) -> dict:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nora_lib
3
- Version: 0.0.4
3
+ Version: 0.0.5.dev1
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 +7,7 @@ dev_requirements = ["mypy", "pytest", "black", "types-requests"]
7
7
 
8
8
  setuptools.setup(
9
9
  name="nora_lib",
10
- version="0.0.4",
10
+ version="0.0.5.dev1",
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,95 +0,0 @@
1
- from datetime import datetime, timezone
2
- from typing import List, Optional
3
- from uuid import UUID
4
-
5
- from nora_lib.interactions.interactions_service import InteractionsService
6
- from nora_lib.interactions.models import (
7
- ReturnedMessage,
8
- ReturnedAgentContextMessage,
9
- ReturnedAgentContextEvent,
10
- EventType,
11
- AgentMessageData,
12
- Event,
13
- )
14
- from nora_lib.context.models import WrappedTaskObject
15
-
16
-
17
- class ContextService:
18
- """
19
- Save and retrieve task agent context from interaction store
20
- """
21
-
22
- def __init__(
23
- self,
24
- agent_actor_id: str, # uuid representing this agent in interaction store
25
- interactions_base_url: str,
26
- interactions_bearer_token: Optional[str],
27
- timeout: int = 30,
28
- ):
29
- self.interactions_service = self._get_interactions_service(
30
- interactions_base_url, interactions_bearer_token, timeout
31
- )
32
- self.agent_actor_id = agent_actor_id
33
-
34
- def _get_interactions_service(self, url, token, timeout) -> InteractionsService:
35
- return InteractionsService(url, timeout, token)
36
-
37
- def get_message(self, message_id: str) -> str:
38
- message: ReturnedMessage = self.interactions_service.get_message(message_id)
39
- if message.annotated_text:
40
- return message.annotated_text
41
- else:
42
- return message.text
43
-
44
- def fetch_context(
45
- self, request: WrappedTaskObject
46
- ) -> List[ReturnedAgentContextMessage]:
47
- message_id = request.message_id
48
-
49
- returned_messages: List[ReturnedMessage] = (
50
- self.interactions_service.fetch_messages_and_events_for_forked_thread(
51
- message_id, EventType.AGENT_CONTEXT
52
- )
53
- )
54
-
55
- messages_with_filtered_events: List[ReturnedAgentContextMessage] = []
56
- for message in returned_messages:
57
- events_saved_by_this_agent: List[ReturnedAgentContextEvent] = []
58
- if message.events:
59
- for event in message.events:
60
- context_event = ReturnedAgentContextEvent.model_validate(event)
61
- if context_event.actor_id == self.agent_actor_id:
62
- events_saved_by_this_agent.append(context_event)
63
-
64
- events_saved_by_this_agent.sort(
65
- key=lambda event: datetime.fromisoformat(event.timestamp)
66
- )
67
-
68
- updated_message = ReturnedAgentContextMessage(
69
- message_id=message.message_id,
70
- actor_id=message.actor_id,
71
- text=message.text,
72
- ts=message.ts,
73
- annotated_text=message.annotated_text,
74
- events=events_saved_by_this_agent,
75
- )
76
-
77
- messages_with_filtered_events.append(updated_message)
78
-
79
- return messages_with_filtered_events
80
-
81
- def save_context(self, event_data: WrappedTaskObject):
82
- agent_data = AgentMessageData(
83
- message_data=event_data.model_dump(),
84
- data_sender_actor_id=event_data.sender_actor_id,
85
- virtual_thread_id=event_data.virtual_thread_id,
86
- )
87
- event = Event(
88
- type=EventType.AGENT_CONTEXT,
89
- actor_id=UUID(self.agent_actor_id),
90
- timestamp=datetime.now(timezone.utc),
91
- data=agent_data.model_dump(),
92
- message_id=event_data.message_id,
93
- )
94
-
95
- self.interactions_service.save_event(event)
@@ -1,21 +0,0 @@
1
- from typing import Generic, Optional, TypeVar
2
- from pydantic import BaseModel, Field
3
-
4
-
5
- R = TypeVar("R", bound=BaseModel)
6
-
7
-
8
- class WrappedTaskObject(BaseModel, Generic[R]):
9
- """Encloses request or response object with additional metadata"""
10
-
11
- message_id: str = Field(
12
- description="id of originating message; key for istore retrieval"
13
- )
14
- sender_actor_id: str = Field(
15
- description="string representation of uuid identifying agent sending data"
16
- )
17
- virtual_thread_id: Optional[str] = Field(
18
- description="Tool-defined local thread to associate follow up requests"
19
- )
20
- task_id: Optional[str] = Field(description="Reference to a long-running task")
21
- data: R = Field(description="Tool-defined request or response")
@@ -1,134 +0,0 @@
1
- from datetime import datetime
2
- import requests
3
- from typing import List, Optional
4
-
5
- from nora_lib.interactions.models import (
6
- Event,
7
- EventType,
8
- ReturnedMessage,
9
- ThreadRelationsResponse,
10
- ThreadForkEventData,
11
- thread_message_lookup_request,
12
- )
13
-
14
-
15
- class InteractionsService:
16
- """
17
- Service which saves interactions to the Interactions API
18
- """
19
-
20
- def __init__(self, base_url, timeout, token):
21
- self.base_url = base_url
22
- self.timeout = timeout
23
- self.headers = {"Authorization": f"Bearer {token}"}
24
-
25
- def save_event(self, event: Event) -> None:
26
- """Save an event to the Interactions API"""
27
- event_url = f"{self.base_url}/interaction/v1/event"
28
- response = requests.post(
29
- event_url,
30
- json=event.model_dump(),
31
- headers=self.headers,
32
- timeout=int(self.timeout),
33
- )
34
- response.raise_for_status()
35
-
36
- def get_message(self, message_id: str) -> ReturnedMessage:
37
- """Fetch a message from the Interactions API"""
38
- message_url = f"{self.base_url}/interaction/v1/search/message"
39
- request_body = {
40
- "id": message_id,
41
- }
42
- response = requests.post(
43
- message_url,
44
- json=request_body,
45
- headers=self.headers,
46
- timeout=int(self.timeout),
47
- )
48
- response.raise_for_status()
49
- res_dict = response.json()["message"]
50
-
51
- return ReturnedMessage.model_validate(res_dict)
52
-
53
- def fetch_thread_messages_and_events_for_message(
54
- self, message_id: str, event_type: str
55
- ) -> ThreadRelationsResponse:
56
- """Fetch messages and associated events from the same thread as provided messagev id"""
57
- message_url = f"{self.base_url}/interaction/v1/search/message"
58
- request_body = thread_message_lookup_request(message_id, event_type=event_type)
59
- response = requests.post(
60
- message_url,
61
- json=request_body,
62
- headers=self.headers,
63
- timeout=int(self.timeout),
64
- )
65
- response.raise_for_status()
66
- json_response = response.json()
67
-
68
- return ThreadRelationsResponse.model_validate(
69
- json_response.get("message", {}).get("thread", {})
70
- )
71
-
72
- def fetch_messages_and_events_for_thread(
73
- self,
74
- thread_id: str,
75
- event_type: Optional[str] = None,
76
- min_timestamp: Optional[str] = None,
77
- ) -> dict:
78
- """Fetch messages and events for the thread containing a given message from the Interactions API"""
79
- THREAD_SEARCH_URL = f"{self.base_url}/interaction/v1/search/thread"
80
- request_body = {
81
- "id": thread_id,
82
- "relations": {
83
- "messages": (
84
- {"filter": {"min_timestamp": min_timestamp}}
85
- if min_timestamp
86
- else {}
87
- ),
88
- "events": {"filter": {"type": event_type}} if event_type else {},
89
- },
90
- }
91
-
92
- response = requests.post(
93
- THREAD_SEARCH_URL,
94
- json=request_body,
95
- headers=self.headers,
96
- timeout=int(self.timeout),
97
- )
98
- response.raise_for_status()
99
- return response.json()
100
-
101
- def fetch_messages_and_events_for_forked_thread(
102
- self, message_id: str, event_type: str
103
- ) -> List[ReturnedMessage]:
104
- """Build a history of messages for a given message including associated events.
105
- This includes messages from pre-forked threads."""
106
- returned_messages: List[ReturnedMessage] = []
107
-
108
- messages_for_thread: ThreadRelationsResponse = (
109
- self.fetch_thread_messages_and_events_for_message(message_id, event_type)
110
- )
111
- if messages_for_thread.messages:
112
- returned_messages.extend(messages_for_thread.messages)
113
-
114
- # Lookup any thread_fork events (conversation across surfaces)
115
- thread_fork_events = self.fetch_messages_and_events_for_thread(
116
- messages_for_thread.thread_id, EventType.THREAD_FORK.value
117
- )
118
- for forked_thread_event in thread_fork_events.get("thread", {}).get(
119
- "events", []
120
- ):
121
- event_data = ThreadForkEventData.model_validate(
122
- forked_thread_event.get("data", {})
123
- )
124
- forked_thread: ThreadRelationsResponse = (
125
- self.fetch_thread_messages_and_events_for_message(
126
- event_data.previous_message_id, event_type
127
- )
128
- )
129
- if forked_thread.messages:
130
- returned_messages.extend(forked_thread.messages)
131
-
132
- returned_messages.sort(key=lambda x: datetime.fromisoformat(x.ts))
133
-
134
- return returned_messages
File without changes
File without changes
File without changes