Skip to content

Commit 87b4bd6

Browse files
committed
Refactor chat module: move TrackedChat implementation to a new file and update imports for clarity
1 parent b33ef0e commit 87b4bd6

4 files changed

Lines changed: 422 additions & 413 deletions

File tree

ldai/chat/__init__.py

Lines changed: 3 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -1,189 +1,6 @@
1-
"""TrackedChat implementation for managing AI chat conversations."""
1+
"""Chat module for LaunchDarkly AI SDK."""
22

3-
import asyncio
4-
from typing import Any, Dict, List, Optional
3+
from ldai.chat.tracked_chat import TrackedChat
54

6-
from ldai.models import AICompletionConfig, LDMessage
7-
from ldai.providers.ai_provider import AIProvider
8-
from ldai.providers.types import ChatResponse, JudgeResponse
9-
from ldai.judge import AIJudge
10-
from ldai.tracker import LDAIConfigTracker
11-
12-
13-
class TrackedChat:
14-
"""
15-
Concrete implementation of TrackedChat that provides chat functionality
16-
by delegating to an AIProvider implementation.
17-
18-
This class handles conversation management and tracking, while delegating
19-
the actual model invocation to the provider.
20-
"""
21-
22-
def __init__(
23-
self,
24-
ai_config: AICompletionConfig,
25-
tracker: LDAIConfigTracker,
26-
provider: AIProvider,
27-
judges: Optional[Dict[str, AIJudge]] = None,
28-
logger: Optional[Any] = None,
29-
):
30-
"""
31-
Initialize the TrackedChat.
32-
33-
:param ai_config: The completion AI configuration
34-
:param tracker: The tracker for the completion configuration
35-
:param provider: The AI provider to use for chat
36-
:param judges: Optional dictionary of judge instances keyed by their configuration keys
37-
:param logger: Optional logger for logging
38-
"""
39-
self._ai_config = ai_config
40-
self._tracker = tracker
41-
self._provider = provider
42-
self._judges = judges or {}
43-
self._logger = logger
44-
self._messages: List[LDMessage] = []
45-
46-
async def invoke(self, prompt: str) -> ChatResponse:
47-
"""
48-
Invoke the chat model with a prompt string.
49-
50-
This method handles conversation management and tracking, delegating to the provider's invoke_model method.
51-
52-
:param prompt: The user prompt to send to the chat model
53-
:return: ChatResponse containing the model's response and metrics
54-
"""
55-
# Convert prompt string to LDMessage with role 'user' and add to conversation history
56-
user_message: LDMessage = LDMessage(role='user', content=prompt)
57-
self._messages.append(user_message)
58-
59-
# Prepend config messages to conversation history for model invocation
60-
config_messages = self._ai_config.messages or []
61-
all_messages = config_messages + self._messages
62-
63-
# Delegate to provider-specific implementation with tracking
64-
response = await self._tracker.track_metrics_of(
65-
lambda result: result.metrics,
66-
lambda: self._provider.invoke_model(all_messages),
67-
)
68-
69-
# Start judge evaluations as async tasks (don't await them)
70-
if (
71-
self._ai_config.judge_configuration
72-
and self._ai_config.judge_configuration.judges
73-
and len(self._ai_config.judge_configuration.judges) > 0
74-
):
75-
evaluation_tasks = self._start_judge_evaluations(self._messages, response)
76-
response.evaluations = evaluation_tasks
77-
78-
# Add the response message to conversation history
79-
self._messages.append(response.message)
80-
return response
81-
82-
def _start_judge_evaluations(
83-
self,
84-
messages: List[LDMessage],
85-
response: ChatResponse,
86-
) -> List[asyncio.Task[Optional[JudgeResponse]]]:
87-
"""
88-
Start judge evaluations as async tasks without awaiting them.
89-
90-
Returns a list of async tasks that can be awaited later.
91-
92-
:param messages: Array of messages representing the conversation history
93-
:param response: The AI response to be evaluated
94-
:return: List of async tasks that will return judge evaluation results
95-
"""
96-
if not self._ai_config.judge_configuration or not self._ai_config.judge_configuration.judges:
97-
return []
98-
99-
judge_configs = self._ai_config.judge_configuration.judges
100-
101-
# Start all judge evaluations as tasks
102-
async def evaluate_judge(judge_config):
103-
judge = self._judges.get(judge_config.key)
104-
if not judge:
105-
if self._logger:
106-
self._logger.warn(
107-
f"Judge configuration is not enabled: {judge_config.key}",
108-
)
109-
return None
110-
111-
eval_result = await judge.evaluate_messages(
112-
messages, response, judge_config.sampling_rate
113-
)
114-
115-
if eval_result and eval_result.success:
116-
self._tracker.track_eval_scores(eval_result.evals)
117-
118-
return eval_result
119-
120-
# Create tasks for each judge evaluation
121-
tasks = [
122-
asyncio.create_task(evaluate_judge(judge_config))
123-
for judge_config in judge_configs
124-
]
125-
126-
return tasks
127-
128-
def get_config(self) -> AICompletionConfig:
129-
"""
130-
Get the underlying AI configuration used to initialize this TrackedChat.
131-
132-
:return: The AI completion configuration
133-
"""
134-
return self._ai_config
135-
136-
def get_tracker(self) -> LDAIConfigTracker:
137-
"""
138-
Get the underlying AI configuration tracker used to initialize this TrackedChat.
139-
140-
:return: The tracker instance
141-
"""
142-
return self._tracker
143-
144-
def get_provider(self) -> AIProvider:
145-
"""
146-
Get the underlying AI provider instance.
147-
148-
This provides direct access to the provider for advanced use cases.
149-
150-
:return: The AI provider instance
151-
"""
152-
return self._provider
153-
154-
def get_judges(self) -> Dict[str, AIJudge]:
155-
"""
156-
Get the judges associated with this TrackedChat.
157-
158-
Returns a dictionary of judge instances keyed by their configuration keys.
159-
160-
:return: Dictionary of judge instances
161-
"""
162-
return self._judges
163-
164-
def append_messages(self, messages: List[LDMessage]) -> None:
165-
"""
166-
Append messages to the conversation history.
167-
168-
Adds messages to the conversation history without invoking the model,
169-
which is useful for managing multi-turn conversations or injecting context.
170-
171-
:param messages: Array of messages to append to the conversation history
172-
"""
173-
self._messages.extend(messages)
174-
175-
def get_messages(self, include_config_messages: bool = False) -> List[LDMessage]:
176-
"""
177-
Get all messages in the conversation history.
178-
179-
:param include_config_messages: Whether to include the config messages from the AIConfig.
180-
Defaults to False.
181-
:return: Array of messages. When include_config_messages is True, returns both config
182-
messages and conversation history with config messages prepended. When False,
183-
returns only the conversation history messages.
184-
"""
185-
if include_config_messages:
186-
config_messages = self._ai_config.messages or []
187-
return config_messages + self._messages
188-
return list(self._messages)
5+
__all__ = ['TrackedChat']
1896

ldai/chat/tracked_chat.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""TrackedChat implementation for managing AI chat conversations."""
2+
3+
import asyncio
4+
from typing import Any, Dict, List, Optional
5+
6+
from ldai.models import AICompletionConfig, LDMessage
7+
from ldai.providers.ai_provider import AIProvider
8+
from ldai.providers.types import ChatResponse, JudgeResponse
9+
from ldai.judge import AIJudge
10+
from ldai.tracker import LDAIConfigTracker
11+
12+
13+
class TrackedChat:
14+
"""
15+
Concrete implementation of TrackedChat that provides chat functionality
16+
by delegating to an AIProvider implementation.
17+
18+
This class handles conversation management and tracking, while delegating
19+
the actual model invocation to the provider.
20+
"""
21+
22+
def __init__(
23+
self,
24+
ai_config: AICompletionConfig,
25+
tracker: LDAIConfigTracker,
26+
provider: AIProvider,
27+
judges: Optional[Dict[str, AIJudge]] = None,
28+
logger: Optional[Any] = None,
29+
):
30+
"""
31+
Initialize the TrackedChat.
32+
33+
:param ai_config: The completion AI configuration
34+
:param tracker: The tracker for the completion configuration
35+
:param provider: The AI provider to use for chat
36+
:param judges: Optional dictionary of judge instances keyed by their configuration keys
37+
:param logger: Optional logger for logging
38+
"""
39+
self._ai_config = ai_config
40+
self._tracker = tracker
41+
self._provider = provider
42+
self._judges = judges or {}
43+
self._logger = logger
44+
self._messages: List[LDMessage] = []
45+
46+
async def invoke(self, prompt: str) -> ChatResponse:
47+
"""
48+
Invoke the chat model with a prompt string.
49+
50+
This method handles conversation management and tracking, delegating to the provider's invoke_model method.
51+
52+
:param prompt: The user prompt to send to the chat model
53+
:return: ChatResponse containing the model's response and metrics
54+
"""
55+
# Convert prompt string to LDMessage with role 'user' and add to conversation history
56+
user_message: LDMessage = LDMessage(role='user', content=prompt)
57+
self._messages.append(user_message)
58+
59+
# Prepend config messages to conversation history for model invocation
60+
config_messages = self._ai_config.messages or []
61+
all_messages = config_messages + self._messages
62+
63+
# Delegate to provider-specific implementation with tracking
64+
response = await self._tracker.track_metrics_of(
65+
lambda result: result.metrics,
66+
lambda: self._provider.invoke_model(all_messages),
67+
)
68+
69+
# Start judge evaluations as async tasks (don't await them)
70+
judge_config = self._ai_config.judge_configuration
71+
if judge_config and judge_config.judges and len(judge_config.judges) > 0:
72+
evaluation_tasks = self._start_judge_evaluations(self._messages, response)
73+
response.evaluations = evaluation_tasks
74+
75+
# Add the response message to conversation history
76+
self._messages.append(response.message)
77+
return response
78+
79+
def _start_judge_evaluations(
80+
self,
81+
messages: List[LDMessage],
82+
response: ChatResponse,
83+
) -> List[asyncio.Task[Optional[JudgeResponse]]]:
84+
"""
85+
Start judge evaluations as async tasks without awaiting them.
86+
87+
Returns a list of async tasks that can be awaited later.
88+
89+
:param messages: Array of messages representing the conversation history
90+
:param response: The AI response to be evaluated
91+
:return: List of async tasks that will return judge evaluation results
92+
"""
93+
if not self._ai_config.judge_configuration or not self._ai_config.judge_configuration.judges:
94+
return []
95+
96+
judge_configs = self._ai_config.judge_configuration.judges
97+
98+
# Start all judge evaluations as tasks
99+
async def evaluate_judge(judge_config):
100+
judge = self._judges.get(judge_config.key)
101+
if not judge:
102+
if self._logger:
103+
self._logger.warn(
104+
f"Judge configuration is not enabled: {judge_config.key}",
105+
)
106+
return None
107+
108+
eval_result = await judge.evaluate_messages(
109+
messages, response, judge_config.sampling_rate
110+
)
111+
112+
if eval_result and eval_result.success:
113+
self._tracker.track_eval_scores(eval_result.evals)
114+
115+
return eval_result
116+
117+
# Create tasks for each judge evaluation
118+
tasks = [
119+
asyncio.create_task(evaluate_judge(judge_config))
120+
for judge_config in judge_configs
121+
]
122+
123+
return tasks
124+
125+
def get_config(self) -> AICompletionConfig:
126+
"""
127+
Get the underlying AI configuration used to initialize this TrackedChat.
128+
129+
:return: The AI completion configuration
130+
"""
131+
return self._ai_config
132+
133+
def get_tracker(self) -> LDAIConfigTracker:
134+
"""
135+
Get the underlying AI configuration tracker used to initialize this TrackedChat.
136+
137+
:return: The tracker instance
138+
"""
139+
return self._tracker
140+
141+
def get_provider(self) -> AIProvider:
142+
"""
143+
Get the underlying AI provider instance.
144+
145+
This provides direct access to the provider for advanced use cases.
146+
147+
:return: The AI provider instance
148+
"""
149+
return self._provider
150+
151+
def get_judges(self) -> Dict[str, AIJudge]:
152+
"""
153+
Get the judges associated with this TrackedChat.
154+
155+
Returns a dictionary of judge instances keyed by their configuration keys.
156+
157+
:return: Dictionary of judge instances
158+
"""
159+
return self._judges
160+
161+
def append_messages(self, messages: List[LDMessage]) -> None:
162+
"""
163+
Append messages to the conversation history.
164+
165+
Adds messages to the conversation history without invoking the model,
166+
which is useful for managing multi-turn conversations or injecting context.
167+
168+
:param messages: Array of messages to append to the conversation history
169+
"""
170+
self._messages.extend(messages)
171+
172+
def get_messages(self, include_config_messages: bool = False) -> List[LDMessage]:
173+
"""
174+
Get all messages in the conversation history.
175+
176+
:param include_config_messages: Whether to include the config messages from the AIConfig.
177+
Defaults to False.
178+
:return: Array of messages. When include_config_messages is True, returns both config
179+
messages and conversation history with config messages prepended. When False,
180+
returns only the conversation history messages.
181+
"""
182+
if include_config_messages:
183+
config_messages = self._ai_config.messages or []
184+
return config_messages + self._messages
185+
return list(self._messages)
186+

0 commit comments

Comments
 (0)