Skip to content

Commit 8029741

Browse files
committed
Allows changing and tracking changes of titles. Updates sharable conversation's title whenever the coordinator's conversation title changes.
1 parent fc2f827 commit 8029741

4 files changed

Lines changed: 126 additions & 47 deletions

File tree

assistants/knowledge-transfer-assistant/assistant/assistant.py

Lines changed: 51 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import asyncio
66
import pathlib
7-
from enum import Enum
87
from typing import Any
98

109
from assistant_extensions import attachments, dashboard_card, navigator
@@ -34,7 +33,7 @@
3433
load_text_include,
3534
)
3635

37-
from .common import detect_assistant_role
36+
from .common import detect_assistant_role, detect_conversation_type, get_shared_conversation_id, ConversationType
3837
from .config import assistant_config
3938
from .conversation_share_link import ConversationKnowledgePackageManager
4039
from .data import InspectorTab, LogEntryType
@@ -98,13 +97,6 @@ async def content_evaluator_factory(
9897

9998
app = assistant.fastapi_app()
10099

101-
102-
class ConversationType(Enum):
103-
COORDINATOR = "coordinator"
104-
TEAM = "team"
105-
SHAREABLE_TEMPLATE = "shareable_template"
106-
107-
108100
@assistant.events.conversation.on_created_including_mine
109101
async def on_conversation_created(context: ConversationContext) -> None:
110102
"""
@@ -113,50 +105,26 @@ async def on_conversation_created(context: ConversationContext) -> None:
113105
2. Shareable Team Conversation: A template conversation that has a share URL and is never directly used
114106
3. Team Conversation(s): Individual conversations for team members created when they redeem the share URL
115107
"""
116-
# Get conversation to access metadata
108+
117109
conversation = await context.get_conversation()
118110
conversation_metadata = conversation.metadata or {}
111+
share_id = conversation_metadata.get("share_id")
119112

120113
config = await assistant_config.get(context.assistant)
114+
conversation_type = detect_conversation_type(conversation)
121115

122-
##
123-
## Figure out what type of conversation this is.
124-
##
125-
126-
conversation_type = ConversationType.COORDINATOR
127-
128-
# Coordinator conversations will not have a share_id or
129-
# is_team_conversation flag in the metadata. So, if they are there, we just
130-
# need to decide if it's a shareable template or a team conversation.
131-
share_id = conversation_metadata.get("share_id")
132-
if conversation_metadata.get("is_team_conversation", False) and share_id:
133-
# If this conversation was imported from another, it indicates it's from
134-
# share redemption.
135-
if conversation.imported_from_conversation_id:
136-
conversation_type = ConversationType.TEAM
137-
# TODO: This might work better for detecting a redeemed link, but
138-
# hasn't been validated.
139-
140-
# if conversation_metadata.get("share_redemption") and conversation_metadata.get("share_redemption").get(
141-
# "conversation_share_id"
142-
# ):
143-
# conversation_type = ConversationType.TEAM
144-
else:
145-
conversation_type = ConversationType.SHAREABLE_TEMPLATE
146-
147-
##
148-
## Handle the conversation based on its type
149-
##
150116
match conversation_type:
151117
case ConversationType.SHAREABLE_TEMPLATE:
118+
119+
# Associate the shareable template with a share ID
152120
if not share_id:
153121
logger.error("No share ID found for shareable team conversation.")
154122
return
155-
156123
await ConversationKnowledgePackageManager.associate_conversation_with_share(context, share_id)
157124
return
158125

159126
case ConversationType.TEAM:
127+
160128
if not share_id:
161129
logger.error("No share ID found for team conversation.")
162130
return
@@ -170,13 +138,9 @@ async def on_conversation_created(context: ConversationContext) -> None:
170138
)
171139

172140
await ConversationKnowledgePackageManager.associate_conversation_with_share(context, share_id)
173-
# Set the conversation role for team conversations
174141
await ConversationKnowledgePackageManager.set_conversation_role(context, share_id, ConversationRole.TEAM)
175-
176-
# Synchronize files.
177142
await ShareManager.synchronize_files_to_team_conversation(context=context, share_id=share_id)
178143

179-
# Generate a welcome message.
180144
welcome_message, debug = await generate_team_welcome_message(context)
181145
await context.send_messages(
182146
NewConversationMessage(
@@ -202,11 +166,10 @@ async def on_conversation_created(context: ConversationContext) -> None:
202166

203167
case ConversationType.COORDINATOR:
204168
try:
169+
# In the beginning, we created a share...
205170
share_id = await KnowledgeTransferManager.create_share(context)
206171

207-
# No default brief - let the state inspector handle displaying instructional content
208-
209-
# Create a team conversation with a share URL
172+
# And it was good. So we then created a sharable conversation that we use as a template.
210173
share_url = await KnowledgeTransferManager.create_shareable_team_conversation(
211174
context=context, share_id=share_id
212175
)
@@ -218,14 +181,52 @@ async def on_conversation_created(context: ConversationContext) -> None:
218181
except Exception as e:
219182
welcome_message = f"I'm having trouble setting up your knowledge transfer. Please try again or contact support if the issue persists. {str(e)}"
220183

221-
# Send the welcome message
222184
await context.send_messages(
223185
NewConversationMessage(
224186
content=welcome_message,
225187
message_type=MessageType.chat,
226188
)
227189
)
228190

191+
# Pop open the inspector panel.
192+
await context.send_conversation_state_event(
193+
AssistantStateEvent(
194+
state_id="brief",
195+
event="focus",
196+
state=None,
197+
)
198+
)
199+
200+
@assistant.events.conversation.on_updated
201+
async def on_conversation_updated(context: ConversationContext) -> None:
202+
"""
203+
Handle conversation updates (including title changes) and sync with shareable template.
204+
"""
205+
try:
206+
conversation = await context.get_conversation()
207+
conversation_type = detect_conversation_type(conversation)
208+
if conversation_type != ConversationType.COORDINATOR:
209+
return
210+
211+
shared_conversation_id = await get_shared_conversation_id(context)
212+
if not shared_conversation_id:
213+
return
214+
215+
# Update the shareable template conversation's title if needed.
216+
try:
217+
target_context = context.for_conversation(shared_conversation_id)
218+
target_conversation = await target_context.get_conversation()
219+
if target_conversation.title != conversation.title:
220+
await target_context.update_conversation_title(conversation.title)
221+
logger.debug(f"Updated conversation {shared_conversation_id} title from '{target_conversation.title}' to '{conversation.title}'")
222+
else:
223+
logger.debug(f"Conversation {shared_conversation_id} title already matches: '{conversation.title}'")
224+
except Exception as title_update_error:
225+
logger.error(f"Error updating conversation {shared_conversation_id} title: {title_update_error}")
226+
227+
except Exception as e:
228+
logger.error(f"Error syncing conversation title: {e}")
229+
229230

230231
@assistant.events.conversation.message.chat.on_created
231232
async def on_message_created(
@@ -545,3 +546,6 @@ async def on_participant_joined(
545546

546547
except Exception as e:
547548
logger.exception(f"Error handling participant join event: {e}")
549+
550+
551+

assistants/knowledge-transfer-assistant/assistant/common.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
helping to reduce code duplication and maintain consistency.
66
"""
77

8+
from enum import Enum
89
from typing import Dict, Optional
910

1011
from semantic_workbench_assistant.assistant_app import ConversationContext
@@ -14,7 +15,35 @@
1415
from .logging import logger
1516
from .storage import ShareStorage
1617
from .storage_models import ConversationRole
18+
from semantic_workbench_api_model.workbench_model import Conversation
1719

20+
class ConversationType(Enum):
21+
COORDINATOR = "coordinator"
22+
TEAM = "team"
23+
SHAREABLE_TEMPLATE = "shareable_template"
24+
25+
def detect_conversation_type(conversation: Conversation) -> ConversationType:
26+
conversation_metadata = conversation.metadata or {}
27+
conversation_type = ConversationType.COORDINATOR
28+
# Coordinator conversations will not have a share_id or
29+
# is_team_conversation flag in the metadata. So, if they are there, we just
30+
# need to decide if it's a shareable template or a team conversation.
31+
share_id = conversation_metadata.get("share_id")
32+
if conversation_metadata.get("is_team_conversation", False) and share_id:
33+
# If this conversation was imported from another, it indicates it's from
34+
# share redemption.
35+
if conversation.imported_from_conversation_id:
36+
conversation_type = ConversationType.TEAM
37+
# TODO: This might work better for detecting a redeemed link, but
38+
# hasn't been validated.
39+
40+
# if conversation_metadata.get("share_redemption") and conversation_metadata.get("share_redemption").get(
41+
# "conversation_share_id"
42+
# ):
43+
# conversation_type = ConversationType.TEAM
44+
else:
45+
conversation_type = ConversationType.SHAREABLE_TEMPLATE
46+
return conversation_type
1847

1948
async def detect_assistant_role(context: ConversationContext) -> ConversationRole:
2049
"""
@@ -45,6 +74,34 @@ async def detect_assistant_role(context: ConversationContext) -> ConversationRol
4574
return ConversationRole.COORDINATOR
4675

4776

77+
async def get_shared_conversation_id(context: ConversationContext) -> Optional[str]:
78+
"""
79+
Get the shared conversation ID for a coordinator conversation.
80+
81+
This utility function retrieves the share ID and finds the associated
82+
shareable template conversation ID from the knowledge package.
83+
84+
Args:
85+
context: The conversation context (should be a coordinator conversation)
86+
87+
Returns:
88+
The shared conversation ID if found, None otherwise
89+
"""
90+
try:
91+
share_id = await ConversationKnowledgePackageManager.get_associated_share_id(context)
92+
if not share_id:
93+
return None
94+
95+
knowledge_package = ShareStorage.read_share(share_id)
96+
if not knowledge_package or not knowledge_package.shared_conversation_id:
97+
return None
98+
99+
return knowledge_package.shared_conversation_id
100+
except Exception as e:
101+
logger.error(f"Error getting shared conversation ID: {e}")
102+
return None
103+
104+
48105
async def log_transfer_action(
49106
context: ConversationContext,
50107
entry_type: LogEntryType,

libraries/python/semantic-workbench-assistant/semantic_workbench_assistant/assistant_app/context.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,17 @@ async def get_conversation(self) -> workbench_model.Conversation:
128128
async def update_conversation(self, metadata: dict[str, Any]) -> workbench_model.Conversation:
129129
return await self._conversation_client.update_conversation(metadata)
130130

131+
async def update_conversation_title(self, title: str) -> workbench_model.Conversation:
132+
"""Update the conversation's title."""
133+
update_data = workbench_model.UpdateConversation(title=title)
134+
http_response = await self._conversation_client._client.patch(
135+
f"/conversations/{self.id}",
136+
json=update_data.model_dump(mode="json", exclude_unset=True, exclude_defaults=True),
137+
headers=self._conversation_client._headers,
138+
)
139+
http_response.raise_for_status()
140+
return workbench_model.Conversation.model_validate(http_response.json())
141+
131142
async def get_participants(self, include_inactive=False) -> workbench_model.ConversationParticipantList:
132143
return await self._conversation_client.get_participants(include_inactive=include_inactive)
133144

libraries/python/semantic-workbench-assistant/semantic_workbench_assistant/assistant_app/service.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,13 @@ async def _forward_event(
696696
file,
697697
)
698698

699+
case workbench_model.ConversationEventType.conversation_updated:
700+
# Conversation metadata updates (title, metadata, etc.)
701+
await self.assistant_app.events.conversation._on_updated_handlers(
702+
True, # event_originated_externally (always True for workbench updates)
703+
conversation_context,
704+
)
705+
699706
@translate_assistant_errors
700707
async def get_conversation_state_descriptions(
701708
self, assistant_id: str, conversation_id: str

0 commit comments

Comments
 (0)