truefoundry 0.9.0rc1__py3-none-any.whl → 0.9.2rc1__py3-none-any.whl
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.
Potentially problematic release.
This version of truefoundry might be problematic. Click here for more details.
- truefoundry/_ask/__init__.py +0 -0
- truefoundry/{deploy/cli/commands/ask_command.py → _ask/cli.py} +7 -11
- truefoundry/{deploy/lib/clients/ask_client.py → _ask/client.py} +314 -199
- truefoundry/_ask/llm_utils.py +344 -0
- truefoundry/cli/__main__.py +1 -1
- truefoundry/cli/display_util.py +1 -63
- truefoundry/cli/util.py +32 -1
- truefoundry/common/constants.py +5 -6
- truefoundry/deploy/cli/commands/__init__.py +16 -1
- truefoundry/deploy/cli/commands/deploy_command.py +2 -1
- truefoundry/deploy/cli/commands/kubeconfig_command.py +1 -2
- truefoundry/deploy/cli/commands/patch_application_command.py +2 -1
- truefoundry/deploy/cli/commands/utils.py +3 -33
- truefoundry/deploy/v2/lib/deployable_patched_models.py +38 -16
- truefoundry/workflow/__init__.py +2 -0
- {truefoundry-0.9.0rc1.dist-info → truefoundry-0.9.2rc1.dist-info}/METADATA +3 -2
- {truefoundry-0.9.0rc1.dist-info → truefoundry-0.9.2rc1.dist-info}/RECORD +19 -17
- {truefoundry-0.9.0rc1.dist-info → truefoundry-0.9.2rc1.dist-info}/WHEEL +0 -0
- {truefoundry-0.9.0rc1.dist-info → truefoundry-0.9.2rc1.dist-info}/entry_points.txt +0 -0
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
try:
|
|
2
2
|
from mcp import ClientSession
|
|
3
3
|
from mcp.client.streamable_http import streamablehttp_client
|
|
4
|
-
from mcp.types import TextContent
|
|
4
|
+
from mcp.types import GetPromptResult, TextContent
|
|
5
5
|
except ImportError:
|
|
6
6
|
import sys
|
|
7
7
|
|
|
8
|
+
message_parts = [
|
|
9
|
+
r'This feature requires the `ai` extra. Please install it using `pip install -U "truefoundry\[ai]"`.',
|
|
10
|
+
"Note: This feature requires Python 3.10 or higher.",
|
|
11
|
+
]
|
|
8
12
|
python_version = sys.version_info
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
if python_version.major < 3 or python_version.minor < 10:
|
|
14
|
+
message_parts.append(
|
|
15
|
+
f"Your current Python version is '{python_version.major}.{python_version.minor}'."
|
|
16
|
+
)
|
|
17
|
+
raise ImportError("\n".join(message_parts)) from None
|
|
13
18
|
|
|
14
19
|
import json
|
|
20
|
+
import uuid
|
|
15
21
|
from contextlib import AsyncExitStack
|
|
16
|
-
from
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
17
24
|
|
|
18
25
|
import rich_click as click
|
|
26
|
+
import yaml
|
|
19
27
|
from openai import NOT_GIVEN, AsyncOpenAI
|
|
20
28
|
from openai.types.chat import (
|
|
21
29
|
ChatCompletionAssistantMessageParam,
|
|
@@ -29,13 +37,29 @@ from pydantic import BaseModel
|
|
|
29
37
|
from rich.console import Console
|
|
30
38
|
from rich.status import Status
|
|
31
39
|
|
|
32
|
-
from truefoundry.
|
|
33
|
-
|
|
40
|
+
from truefoundry._ask.llm_utils import (
|
|
41
|
+
log_chat_completion_message,
|
|
42
|
+
translate_tools_for_gemini,
|
|
43
|
+
)
|
|
44
|
+
from truefoundry.common.constants import ENV_VARS, TFY_CONFIG_DIR
|
|
34
45
|
from truefoundry.logger import logger
|
|
35
46
|
|
|
36
47
|
console = Console(soft_wrap=False)
|
|
37
48
|
|
|
38
49
|
|
|
50
|
+
def _get_content(data: Any) -> Optional[Any]:
|
|
51
|
+
content = None
|
|
52
|
+
if isinstance(data, dict):
|
|
53
|
+
content = (
|
|
54
|
+
data.get("content", {}).get("text")
|
|
55
|
+
if isinstance(data.get("content"), dict)
|
|
56
|
+
else data.get("content")
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
content = data
|
|
60
|
+
return content
|
|
61
|
+
|
|
62
|
+
|
|
39
63
|
class AskClient:
|
|
40
64
|
"""Handles the chat session lifecycle between the user and the assistant via OpenAI and MCP."""
|
|
41
65
|
|
|
@@ -52,77 +76,221 @@ class AskClient:
|
|
|
52
76
|
self.debug = debug
|
|
53
77
|
|
|
54
78
|
self.async_openai_client = openai_client or AsyncOpenAI()
|
|
55
|
-
# Initialize the OpenAI client with the session
|
|
56
79
|
self.openai_model = openai_model
|
|
80
|
+
self.generation_params: Dict[str, Any] = dict(
|
|
81
|
+
ENV_VARS.TFY_ASK_GENERATION_PARAMS
|
|
82
|
+
)
|
|
57
83
|
self._log_message(
|
|
58
|
-
f"\
|
|
84
|
+
f"\nInitialized with model: {self.openai_model!r}, "
|
|
85
|
+
f"generation_params: {self.generation_params!r}, "
|
|
86
|
+
f"base_url: {str(self.async_openai_client.base_url)!r}\n",
|
|
59
87
|
)
|
|
60
|
-
|
|
61
|
-
self.exit_stack = AsyncExitStack()
|
|
62
88
|
self.history: List[ChatCompletionMessageParam] = []
|
|
63
89
|
self._cached_tools: Optional[List[ChatCompletionToolParam]] = None
|
|
90
|
+
self._session_id = str(uuid.uuid4())
|
|
91
|
+
self._prompt_length = 0
|
|
64
92
|
|
|
65
|
-
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
self.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
def _auth_headers(self):
|
|
94
|
+
"""Generate authorization headers for connecting to the SSE server."""
|
|
95
|
+
return {
|
|
96
|
+
"Authorization": f"Bearer {self.token}",
|
|
97
|
+
"X-TFY-Cluster-Id": self.cluster,
|
|
98
|
+
"X-TFY-Session-Id": self._session_id,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
def _format_tool_result(self, content) -> str:
|
|
102
|
+
"""Format tool result into a readable string or JSON block."""
|
|
103
|
+
if isinstance(content, list):
|
|
104
|
+
content = (
|
|
105
|
+
content[0].text
|
|
106
|
+
if len(content) == 1 and isinstance(content[0], TextContent)
|
|
107
|
+
else content
|
|
79
108
|
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
109
|
+
if isinstance(content, list):
|
|
110
|
+
return (
|
|
111
|
+
"```\n"
|
|
112
|
+
+ "\n".join(
|
|
113
|
+
(
|
|
114
|
+
item.model_dump_json(indent=2)
|
|
115
|
+
if isinstance(item, BaseModel)
|
|
116
|
+
else str(item)
|
|
117
|
+
)
|
|
118
|
+
for item in content
|
|
119
|
+
)
|
|
120
|
+
+ "\n```"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if isinstance(content, (BaseModel, dict)):
|
|
124
|
+
return (
|
|
125
|
+
"```\n"
|
|
126
|
+
+ json.dumps(
|
|
127
|
+
content.model_dump() if isinstance(content, BaseModel) else content,
|
|
128
|
+
indent=2,
|
|
129
|
+
)
|
|
130
|
+
+ "\n```"
|
|
86
131
|
)
|
|
87
132
|
|
|
88
|
-
|
|
133
|
+
if isinstance(content, str):
|
|
134
|
+
try:
|
|
135
|
+
return "```\n" + json.dumps(json.loads(content), indent=2) + "\n```"
|
|
136
|
+
except Exception:
|
|
137
|
+
return content
|
|
89
138
|
|
|
90
|
-
|
|
91
|
-
self._log_message(f"❌ Connection error: {e}")
|
|
92
|
-
await self.cleanup()
|
|
93
|
-
raise
|
|
94
|
-
finally:
|
|
95
|
-
await self.exit_stack.__aenter__()
|
|
139
|
+
return str(content)
|
|
96
140
|
|
|
97
|
-
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
getattr(self, "_streams_context", None),
|
|
102
|
-
]:
|
|
103
|
-
if context:
|
|
104
|
-
await context.__aexit__(None, None, None)
|
|
141
|
+
def _append_message(self, message: ChatCompletionMessageParam, log: bool = True):
|
|
142
|
+
"""Append a message to history and optionally log it."""
|
|
143
|
+
self._log_message(message, log)
|
|
144
|
+
self.history.append(message)
|
|
105
145
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
146
|
+
def _log_message(
|
|
147
|
+
self,
|
|
148
|
+
message: Union[str, ChatCompletionMessageParam],
|
|
149
|
+
log: bool = False,
|
|
150
|
+
):
|
|
151
|
+
"""Display a message using Rich console, conditionally based on debug settings."""
|
|
152
|
+
if not self.debug and not log:
|
|
153
|
+
return
|
|
154
|
+
if isinstance(message, str):
|
|
155
|
+
console.print(message)
|
|
156
|
+
else:
|
|
157
|
+
log_chat_completion_message(message, console=console)
|
|
109
158
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
159
|
+
def _maybe_save_transcript(self):
|
|
160
|
+
"""Save the transcript to a file."""
|
|
161
|
+
if not click.confirm("Save the chat transcript to a file?", default=True):
|
|
162
|
+
return
|
|
163
|
+
transcripts_dir = TFY_CONFIG_DIR / "tfy-ask-transcripts"
|
|
164
|
+
transcripts_dir.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
transcript_file = (
|
|
166
|
+
transcripts_dir
|
|
167
|
+
/ f"chat-{self._session_id}-{datetime.now().strftime('%Y-%m-%dT%H-%M-%S')}.json"
|
|
168
|
+
)
|
|
116
169
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
170
|
+
def _custom_encoder(obj):
|
|
171
|
+
if isinstance(obj, datetime):
|
|
172
|
+
return obj.isoformat()
|
|
173
|
+
elif isinstance(obj, BaseModel):
|
|
174
|
+
return obj.model_dump()
|
|
175
|
+
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
|
|
176
|
+
|
|
177
|
+
data = {
|
|
178
|
+
"cluster": self.cluster,
|
|
179
|
+
"model": self.openai_model,
|
|
180
|
+
"messages": self.history,
|
|
181
|
+
}
|
|
182
|
+
try:
|
|
183
|
+
with open(transcript_file, "w") as f:
|
|
184
|
+
json.dump(data, f, indent=2, default=_custom_encoder)
|
|
185
|
+
console.print(f"Chat transcript saved to {transcript_file}")
|
|
186
|
+
except (IOError, PermissionError) as e:
|
|
187
|
+
console.print(f"[red]Failed to save transcript: {e}[/red]")
|
|
188
|
+
|
|
189
|
+
def _load_config_from_file(
|
|
190
|
+
self, filepath: str
|
|
191
|
+
) -> Tuple[Optional[GetPromptResult], str, Dict[str, Any]]:
|
|
192
|
+
self._log_message(
|
|
193
|
+
f"Loading initial prompt and params from file {filepath}.", log=True
|
|
194
|
+
)
|
|
195
|
+
with open(filepath, "r") as f:
|
|
196
|
+
content = yaml.safe_load(f)
|
|
197
|
+
if "prompt" in content:
|
|
198
|
+
prompt_result = GetPromptResult.model_validate(content["prompt"])
|
|
199
|
+
else:
|
|
200
|
+
prompt_result = None
|
|
201
|
+
model = content.get("model") or self.openai_model
|
|
202
|
+
generation_params = (
|
|
203
|
+
content.get("generation_params", {}) or self.generation_params
|
|
204
|
+
)
|
|
205
|
+
return prompt_result, model, generation_params
|
|
120
206
|
|
|
121
|
-
|
|
207
|
+
def _maybe_reload_prompt_and_params(self):
|
|
208
|
+
if ENV_VARS.TFY_INTERNAL_ASK_CONFIG_OVERRIDE_FILE:
|
|
209
|
+
prompt_result, model, generation_params = self._load_config_from_file(
|
|
210
|
+
filepath=ENV_VARS.TFY_INTERNAL_ASK_CONFIG_OVERRIDE_FILE
|
|
211
|
+
)
|
|
212
|
+
self.openai_model = model
|
|
213
|
+
self.generation_params = generation_params
|
|
214
|
+
if prompt_result:
|
|
215
|
+
message_index = 0
|
|
216
|
+
for message in prompt_result.messages:
|
|
217
|
+
data = (
|
|
218
|
+
message.model_dump()
|
|
219
|
+
if isinstance(message, BaseModel)
|
|
220
|
+
else message
|
|
221
|
+
)
|
|
222
|
+
content = _get_content(data)
|
|
223
|
+
# First message is system prompt?
|
|
224
|
+
if content:
|
|
225
|
+
if message_index < self._prompt_length:
|
|
226
|
+
self.history[message_index] = (
|
|
227
|
+
ChatCompletionSystemMessageParam(
|
|
228
|
+
role="system", content=content
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
# insert just before self._prompt_length
|
|
233
|
+
self.history.insert(
|
|
234
|
+
self._prompt_length,
|
|
235
|
+
ChatCompletionSystemMessageParam(
|
|
236
|
+
role="system", content=content
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
message_index += 1
|
|
240
|
+
# Remove any leftover old prompt lines
|
|
241
|
+
del self.history[message_index : self._prompt_length]
|
|
242
|
+
self._prompt_length = message_index
|
|
243
|
+
self._log_message(
|
|
244
|
+
f"Reloaded initial prompt and params from {ENV_VARS.TFY_INTERNAL_ASK_CONFIG_OVERRIDE_FILE}.",
|
|
245
|
+
log=True,
|
|
246
|
+
)
|
|
122
247
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
248
|
+
async def _call_openai(self, tools: Optional[List[ChatCompletionToolParam]]):
|
|
249
|
+
"""Make a chat completion request to OpenAI with optional tool support."""
|
|
250
|
+
return await self.async_openai_client.chat.completions.create(
|
|
251
|
+
model=self.openai_model,
|
|
252
|
+
messages=self.history,
|
|
253
|
+
tools=tools or NOT_GIVEN,
|
|
254
|
+
extra_headers={
|
|
255
|
+
"X-TFY-METADATA": json.dumps(
|
|
256
|
+
{
|
|
257
|
+
"tfy_log_request": "true",
|
|
258
|
+
"session_id": self._session_id,
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
},
|
|
262
|
+
**self.generation_params,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
async def _handle_tool_calls(self, message, spinner: Status):
|
|
266
|
+
"""Execute tool calls returned by the assistant and return the results."""
|
|
267
|
+
for tool_call in message.tool_calls:
|
|
268
|
+
try:
|
|
269
|
+
spinner.update(
|
|
270
|
+
f"Executing tool: {tool_call.function.name}", spinner="aesthetic"
|
|
271
|
+
)
|
|
272
|
+
args = json.loads(tool_call.function.arguments)
|
|
273
|
+
result = await self.session.call_tool(tool_call.function.name, args)
|
|
274
|
+
content = getattr(result, "content", result)
|
|
275
|
+
result_content = self._format_tool_result(content)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
result_content = f"Tool `{tool_call.function.name}` call failed: {e}"
|
|
278
|
+
|
|
279
|
+
# Log assistant's tool call
|
|
280
|
+
self._append_message(
|
|
281
|
+
ChatCompletionAssistantMessageParam(
|
|
282
|
+
role="assistant", content=None, tool_calls=[tool_call]
|
|
283
|
+
),
|
|
284
|
+
log=self.debug,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Log tool response
|
|
288
|
+
self._append_message(
|
|
289
|
+
ChatCompletionToolMessageParam(
|
|
290
|
+
role="tool", tool_call_id=tool_call.id, content=result_content
|
|
291
|
+
),
|
|
292
|
+
log=self.debug,
|
|
293
|
+
)
|
|
126
294
|
|
|
127
295
|
async def process_query(self, query: Optional[str] = None, max_turns: int = 50):
|
|
128
296
|
"""Handles sending user input to the assistant and processing the assistant’s reply."""
|
|
@@ -142,35 +310,33 @@ class AskClient:
|
|
|
142
310
|
while True:
|
|
143
311
|
try:
|
|
144
312
|
if turn >= max_turns:
|
|
145
|
-
self._log_message("Max turns reached. Exiting.")
|
|
313
|
+
self._log_message("Max turns reached. Exiting.", log=True)
|
|
146
314
|
break
|
|
147
315
|
spinner.update("Thinking...", spinner="dots")
|
|
148
|
-
response = await self._call_openai(
|
|
149
|
-
model=self.openai_model, tools=tools
|
|
150
|
-
)
|
|
316
|
+
response = await self._call_openai(tools=tools)
|
|
151
317
|
turn += 1
|
|
152
318
|
message = response.choices[0].message
|
|
153
319
|
|
|
154
|
-
if message.
|
|
155
|
-
await self._handle_tool_calls(message, spinner)
|
|
156
|
-
elif message.content:
|
|
320
|
+
if message.content:
|
|
157
321
|
self._append_message(
|
|
158
322
|
ChatCompletionAssistantMessageParam(
|
|
159
323
|
role="assistant", content=message.content
|
|
160
324
|
)
|
|
161
325
|
)
|
|
162
|
-
|
|
163
|
-
|
|
326
|
+
|
|
327
|
+
if message.tool_calls:
|
|
328
|
+
await self._handle_tool_calls(message, spinner)
|
|
329
|
+
|
|
330
|
+
if not message.content and not message.tool_calls:
|
|
164
331
|
self._log_message("No assistant response.")
|
|
165
332
|
break
|
|
166
333
|
except Exception as e:
|
|
167
|
-
self._log_message(f"OpenAI call failed: {e}", log=
|
|
334
|
+
self._log_message(f"OpenAI call failed: {e}", log=True)
|
|
168
335
|
console.print(
|
|
169
336
|
"Something went wrong. Please try rephrasing your query."
|
|
170
337
|
)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
] # Revert to safe state
|
|
338
|
+
# Revert to safe state
|
|
339
|
+
self.history = self.history[:_checkpoint_idx]
|
|
174
340
|
turn = 0
|
|
175
341
|
break
|
|
176
342
|
|
|
@@ -179,22 +345,27 @@ class AskClient:
|
|
|
179
345
|
if self._cached_tools:
|
|
180
346
|
return self._cached_tools
|
|
181
347
|
|
|
182
|
-
|
|
348
|
+
tools: List[ChatCompletionToolParam] = [
|
|
183
349
|
{
|
|
184
350
|
"type": "function",
|
|
185
351
|
"function": {
|
|
186
352
|
"name": t.name,
|
|
187
|
-
"description": t.description,
|
|
353
|
+
"description": t.description or "",
|
|
188
354
|
"parameters": t.inputSchema,
|
|
189
355
|
},
|
|
190
356
|
}
|
|
191
357
|
for t in (await self.session.list_tools()).tools
|
|
192
358
|
]
|
|
193
359
|
|
|
360
|
+
if "gemini" in self.openai_model.lower():
|
|
361
|
+
tools = translate_tools_for_gemini(tools) or []
|
|
362
|
+
|
|
363
|
+
self._cached_tools = tools
|
|
364
|
+
|
|
194
365
|
self._log_message("\nAvailable tools:")
|
|
195
366
|
for tool in self._cached_tools or []:
|
|
196
367
|
self._log_message(
|
|
197
|
-
f" - {tool['function']['name']}: {tool['function']['description']}"
|
|
368
|
+
f" - {tool['function']['name']}: {tool['function']['description'] or ''}"
|
|
198
369
|
)
|
|
199
370
|
return self._cached_tools
|
|
200
371
|
|
|
@@ -203,140 +374,87 @@ class AskClient:
|
|
|
203
374
|
if not (self.session and prompt_name):
|
|
204
375
|
return
|
|
205
376
|
|
|
206
|
-
|
|
207
|
-
if
|
|
377
|
+
prompt_result = None
|
|
378
|
+
if ENV_VARS.TFY_INTERNAL_ASK_CONFIG_OVERRIDE_FILE:
|
|
379
|
+
prompt_result, model, generation_params = self._load_config_from_file(
|
|
380
|
+
filepath=ENV_VARS.TFY_INTERNAL_ASK_CONFIG_OVERRIDE_FILE
|
|
381
|
+
)
|
|
382
|
+
self.openai_model = model
|
|
383
|
+
self.generation_params = generation_params
|
|
384
|
+
|
|
385
|
+
if not prompt_result:
|
|
386
|
+
prompt_result = await self.session.get_prompt(name=prompt_name)
|
|
387
|
+
|
|
388
|
+
if not prompt_result:
|
|
208
389
|
self._log_message("Failed to get initial system prompt.")
|
|
209
390
|
return
|
|
210
391
|
|
|
211
|
-
for message in
|
|
392
|
+
for message in prompt_result.messages:
|
|
212
393
|
data = message.model_dump() if isinstance(message, BaseModel) else message
|
|
213
|
-
content =
|
|
214
|
-
if isinstance(data, dict):
|
|
215
|
-
content = (
|
|
216
|
-
data.get("content", {}).get("text")
|
|
217
|
-
if isinstance(data.get("content"), dict)
|
|
218
|
-
else data.get("content")
|
|
219
|
-
)
|
|
220
|
-
else:
|
|
221
|
-
content = data
|
|
222
|
-
|
|
394
|
+
content = _get_content(data)
|
|
223
395
|
# First message is system prompt?
|
|
224
|
-
|
|
225
396
|
if content:
|
|
226
397
|
self._append_message(
|
|
227
398
|
ChatCompletionSystemMessageParam(role="system", content=content),
|
|
228
399
|
log=self.debug,
|
|
229
400
|
)
|
|
401
|
+
self._prompt_length = len(self.history)
|
|
230
402
|
|
|
231
|
-
async def
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
tools=tools or NOT_GIVEN,
|
|
239
|
-
temperature=0.0, # Set to 0 for deterministic behavior
|
|
240
|
-
top_p=1,
|
|
403
|
+
async def chat_loop(self):
|
|
404
|
+
"""Interactive loop: accepts user queries and returns responses until interrupted or 'exit' is typed."""
|
|
405
|
+
self._append_message(
|
|
406
|
+
ChatCompletionAssistantMessageParam(
|
|
407
|
+
role="assistant",
|
|
408
|
+
content="Hello! How can I help you with your Kubernetes cluster?",
|
|
409
|
+
)
|
|
241
410
|
)
|
|
242
411
|
|
|
243
|
-
|
|
244
|
-
"""Execute tool calls returned by the assistant and return the results."""
|
|
245
|
-
for tool_call in message.tool_calls:
|
|
412
|
+
while True:
|
|
246
413
|
try:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
result = await self.session.call_tool(tool_call.function.name, args)
|
|
252
|
-
content = getattr(result, "content", result)
|
|
253
|
-
result_content = self._format_tool_result(content)
|
|
254
|
-
except Exception as e:
|
|
255
|
-
result_content = f"Tool `{tool_call.function.name}` call failed: {e}"
|
|
256
|
-
|
|
257
|
-
# Log assistant's tool call
|
|
258
|
-
self._append_message(
|
|
259
|
-
ChatCompletionAssistantMessageParam(
|
|
260
|
-
role="assistant", content=None, tool_calls=[tool_call]
|
|
261
|
-
),
|
|
262
|
-
log=self.debug,
|
|
263
|
-
)
|
|
414
|
+
query = click.prompt(click.style("User", fg="yellow"), type=str)
|
|
415
|
+
if not query:
|
|
416
|
+
self._log_message("Empty query. Type 'exit' to quit.", log=True)
|
|
417
|
+
continue
|
|
264
418
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
),
|
|
270
|
-
log=self.debug,
|
|
271
|
-
)
|
|
419
|
+
if query.lower() in ("exit", "quit"):
|
|
420
|
+
self._log_message("Exiting chat...")
|
|
421
|
+
self._maybe_save_transcript()
|
|
422
|
+
break
|
|
272
423
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if isinstance(content, list):
|
|
276
|
-
content = (
|
|
277
|
-
content[0].text
|
|
278
|
-
if len(content) == 1 and isinstance(content[0], TextContent)
|
|
279
|
-
else content
|
|
280
|
-
)
|
|
281
|
-
if isinstance(content, list):
|
|
282
|
-
return (
|
|
283
|
-
"```\n"
|
|
284
|
-
+ "\n".join(
|
|
285
|
-
(
|
|
286
|
-
item.model_dump_json(indent=2)
|
|
287
|
-
if isinstance(item, BaseModel)
|
|
288
|
-
else str(item)
|
|
289
|
-
)
|
|
290
|
-
for item in content
|
|
291
|
-
)
|
|
292
|
-
+ "\n```"
|
|
293
|
-
)
|
|
424
|
+
self._maybe_reload_prompt_and_params()
|
|
425
|
+
await self.process_query(query)
|
|
294
426
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
content.model_dump() if isinstance(content, BaseModel) else content,
|
|
300
|
-
indent=2,
|
|
301
|
-
)
|
|
302
|
-
+ "\n```"
|
|
303
|
-
)
|
|
427
|
+
except (KeyboardInterrupt, EOFError, click.Abort):
|
|
428
|
+
self._log_message("\nChat interrupted. Exiting chat...")
|
|
429
|
+
self._maybe_save_transcript()
|
|
430
|
+
break
|
|
304
431
|
|
|
305
|
-
|
|
432
|
+
async def start_session(self, server_url: str, prompt_name: Optional[str] = None):
|
|
433
|
+
"""Initialize connection to the Streamable HTTP MCP server and prepare the chat session."""
|
|
434
|
+
logger.debug(f"Starting a new client for {server_url}")
|
|
435
|
+
async with AsyncExitStack() as exit_stack:
|
|
306
436
|
try:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def _log_message(
|
|
326
|
-
self,
|
|
327
|
-
message: Union[str, ChatCompletionMessageParam],
|
|
328
|
-
log: bool = False,
|
|
329
|
-
):
|
|
330
|
-
"""Display a message using Rich console, conditionally based on debug settings."""
|
|
331
|
-
if not self.debug and not log:
|
|
332
|
-
return
|
|
333
|
-
if isinstance(message, str):
|
|
334
|
-
console.print(message)
|
|
335
|
-
else:
|
|
336
|
-
log_chat_completion_message(message, console_=console)
|
|
437
|
+
read_stream, write_stream, _ = await exit_stack.enter_async_context(
|
|
438
|
+
streamablehttp_client(url=server_url, headers=self._auth_headers())
|
|
439
|
+
)
|
|
440
|
+
self.session = await exit_stack.enter_async_context(
|
|
441
|
+
ClientSession(read_stream=read_stream, write_stream=write_stream)
|
|
442
|
+
)
|
|
443
|
+
await self.session.initialize()
|
|
444
|
+
except Exception as e:
|
|
445
|
+
self._log_message(f"❌ Connection error: {e}", log=True)
|
|
446
|
+
raise
|
|
447
|
+
else:
|
|
448
|
+
self._log_message(f"✅ Connected to {server_url}")
|
|
449
|
+
await self._list_tools()
|
|
450
|
+
await self._load_initial_prompt(prompt_name)
|
|
451
|
+
self._log_message(
|
|
452
|
+
"\n[dim]Tip: Type 'exit' to quit chat.[/dim]", log=True
|
|
453
|
+
)
|
|
454
|
+
await self.chat_loop()
|
|
337
455
|
|
|
338
456
|
|
|
339
|
-
async def
|
|
457
|
+
async def ask_client(
|
|
340
458
|
cluster: str,
|
|
341
459
|
server_url: str,
|
|
342
460
|
token: str,
|
|
@@ -353,15 +471,12 @@ async def ask_client_main(
|
|
|
353
471
|
openai_model=openai_model,
|
|
354
472
|
)
|
|
355
473
|
try:
|
|
356
|
-
await ask_client.
|
|
474
|
+
await ask_client.start_session(
|
|
357
475
|
server_url=server_url, prompt_name=ENV_VARS.TFY_ASK_SYSTEM_PROMPT_NAME
|
|
358
476
|
)
|
|
359
|
-
|
|
477
|
+
except KeyboardInterrupt:
|
|
478
|
+
console.print("[yellow]Chat interrupted.[/yellow]")
|
|
360
479
|
except Exception as e:
|
|
361
480
|
console.print(
|
|
362
|
-
f"[red]An unexpected error occurred while running the assistant: {e}[/red]
|
|
481
|
+
f"[red]An unexpected error occurred while running the assistant: {e}[/red]\nCheck with TrueFoundry support for more details."
|
|
363
482
|
)
|
|
364
|
-
except KeyboardInterrupt:
|
|
365
|
-
console.print("[yellow]Chat interrupted.[/yellow]")
|
|
366
|
-
finally:
|
|
367
|
-
await ask_client.cleanup()
|