fast-agent-mcp 0.2.35__py3-none-any.whl → 0.2.37__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 fast-agent-mcp might be problematic. Click here for more details.

Files changed (73) hide show
  1. {fast_agent_mcp-0.2.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/METADATA +15 -12
  2. {fast_agent_mcp-0.2.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/RECORD +55 -56
  3. {fast_agent_mcp-0.2.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/licenses/LICENSE +1 -1
  4. mcp_agent/agents/base_agent.py +2 -2
  5. mcp_agent/agents/workflow/router_agent.py +1 -1
  6. mcp_agent/cli/commands/quickstart.py +59 -5
  7. mcp_agent/config.py +10 -0
  8. mcp_agent/context.py +1 -4
  9. mcp_agent/core/agent_types.py +7 -6
  10. mcp_agent/core/direct_decorators.py +14 -0
  11. mcp_agent/core/direct_factory.py +1 -0
  12. mcp_agent/core/enhanced_prompt.py +73 -13
  13. mcp_agent/core/fastagent.py +23 -2
  14. mcp_agent/core/interactive_prompt.py +118 -8
  15. mcp_agent/human_input/elicitation_form.py +723 -0
  16. mcp_agent/human_input/elicitation_forms.py +59 -0
  17. mcp_agent/human_input/elicitation_handler.py +88 -0
  18. mcp_agent/human_input/elicitation_state.py +34 -0
  19. mcp_agent/llm/augmented_llm.py +31 -0
  20. mcp_agent/llm/providers/augmented_llm_anthropic.py +11 -23
  21. mcp_agent/llm/providers/augmented_llm_azure.py +4 -4
  22. mcp_agent/llm/providers/augmented_llm_google_native.py +4 -2
  23. mcp_agent/llm/providers/augmented_llm_openai.py +195 -12
  24. mcp_agent/llm/providers/multipart_converter_openai.py +4 -3
  25. mcp_agent/mcp/elicitation_factory.py +84 -0
  26. mcp_agent/mcp/elicitation_handlers.py +155 -0
  27. mcp_agent/mcp/helpers/content_helpers.py +27 -0
  28. mcp_agent/mcp/helpers/server_config_helpers.py +10 -8
  29. mcp_agent/mcp/interfaces.py +1 -1
  30. mcp_agent/mcp/mcp_agent_client_session.py +44 -1
  31. mcp_agent/mcp/mcp_aggregator.py +56 -11
  32. mcp_agent/mcp/mcp_connection_manager.py +30 -18
  33. mcp_agent/mcp_server/agent_server.py +2 -0
  34. mcp_agent/mcp_server_registry.py +16 -8
  35. mcp_agent/resources/examples/data-analysis/analysis.py +1 -2
  36. mcp_agent/resources/examples/mcp/elicitations/README.md +157 -0
  37. mcp_agent/resources/examples/mcp/elicitations/elicitation_account_server.py +88 -0
  38. mcp_agent/resources/examples/mcp/elicitations/elicitation_forms_server.py +232 -0
  39. mcp_agent/resources/examples/mcp/elicitations/elicitation_game_server.py +164 -0
  40. mcp_agent/resources/examples/mcp/elicitations/fastagent.config.yaml +35 -0
  41. mcp_agent/resources/examples/mcp/elicitations/fastagent.secrets.yaml.example +17 -0
  42. mcp_agent/resources/examples/mcp/elicitations/forms_demo.py +111 -0
  43. mcp_agent/resources/examples/mcp/elicitations/game_character.py +65 -0
  44. mcp_agent/resources/examples/mcp/elicitations/game_character_handler.py +256 -0
  45. mcp_agent/resources/examples/{prompting/agent.py → mcp/elicitations/tool_call.py} +4 -5
  46. mcp_agent/resources/examples/mcp/state-transfer/agent_two.py +1 -1
  47. mcp_agent/resources/examples/mcp/state-transfer/fastagent.config.yaml +1 -1
  48. mcp_agent/resources/examples/mcp/state-transfer/fastagent.secrets.yaml.example +1 -0
  49. mcp_agent/resources/examples/workflows/evaluator.py +1 -1
  50. mcp_agent/resources/examples/workflows/graded_report.md +89 -0
  51. mcp_agent/resources/examples/workflows/orchestrator.py +7 -9
  52. mcp_agent/resources/examples/workflows/parallel.py +0 -2
  53. mcp_agent/resources/examples/workflows/short_story.md +13 -0
  54. mcp_agent/resources/examples/in_dev/agent_build.py +0 -84
  55. mcp_agent/resources/examples/in_dev/css-LICENSE.txt +0 -21
  56. mcp_agent/resources/examples/in_dev/slides.py +0 -110
  57. mcp_agent/resources/examples/internal/agent.py +0 -20
  58. mcp_agent/resources/examples/internal/fastagent.config.yaml +0 -66
  59. mcp_agent/resources/examples/internal/history_transfer.py +0 -35
  60. mcp_agent/resources/examples/internal/job.py +0 -84
  61. mcp_agent/resources/examples/internal/prompt_category.py +0 -21
  62. mcp_agent/resources/examples/internal/prompt_sizing.py +0 -51
  63. mcp_agent/resources/examples/internal/simple.txt +0 -2
  64. mcp_agent/resources/examples/internal/sizer.py +0 -20
  65. mcp_agent/resources/examples/internal/social.py +0 -67
  66. mcp_agent/resources/examples/prompting/__init__.py +0 -3
  67. mcp_agent/resources/examples/prompting/delimited_prompt.txt +0 -14
  68. mcp_agent/resources/examples/prompting/fastagent.config.yaml +0 -43
  69. mcp_agent/resources/examples/prompting/image_server.py +0 -52
  70. mcp_agent/resources/examples/prompting/prompt1.txt +0 -6
  71. mcp_agent/resources/examples/prompting/work_with_image.py +0 -19
  72. {fast_agent_mcp-0.2.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/WHEEL +0 -0
  73. {fast_agent_mcp-0.2.35.dist-info → fast_agent_mcp-0.2.37.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,723 @@
1
+ """Simplified, robust elicitation form dialog."""
2
+
3
+ from datetime import date, datetime
4
+ from typing import Any, Dict, Optional
5
+
6
+ from mcp.types import ElicitRequestedSchema
7
+ from prompt_toolkit import Application
8
+ from prompt_toolkit.buffer import Buffer
9
+ from prompt_toolkit.filters import Condition
10
+ from prompt_toolkit.formatted_text import FormattedText
11
+ from prompt_toolkit.key_binding import KeyBindings
12
+ from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
13
+ from prompt_toolkit.layout import HSplit, Layout, ScrollablePane, VSplit, Window
14
+ from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
15
+ from prompt_toolkit.validation import ValidationError, Validator
16
+ from prompt_toolkit.widgets import (
17
+ Button,
18
+ Checkbox,
19
+ Dialog,
20
+ Frame,
21
+ Label,
22
+ RadioList,
23
+ )
24
+ from pydantic import AnyUrl, EmailStr
25
+ from pydantic import ValidationError as PydanticValidationError
26
+
27
+ from mcp_agent.human_input.elicitation_forms import ELICITATION_STYLE
28
+ from mcp_agent.human_input.elicitation_state import elicitation_state
29
+
30
+
31
+ class SimpleNumberValidator(Validator):
32
+ """Simple number validator with real-time feedback."""
33
+
34
+ def __init__(
35
+ self, field_type: str, minimum: Optional[float] = None, maximum: Optional[float] = None
36
+ ):
37
+ self.field_type = field_type
38
+ self.minimum = minimum
39
+ self.maximum = maximum
40
+
41
+ def validate(self, document):
42
+ text = document.text.strip()
43
+ if not text:
44
+ return # Empty is OK for optional fields
45
+
46
+ try:
47
+ if self.field_type == "integer":
48
+ value = int(text)
49
+ else:
50
+ value = float(text)
51
+
52
+ if self.minimum is not None and value < self.minimum:
53
+ raise ValidationError(
54
+ message=f"Must be ≥ {self.minimum}", cursor_position=len(text)
55
+ )
56
+
57
+ if self.maximum is not None and value > self.maximum:
58
+ raise ValidationError(
59
+ message=f"Must be ≤ {self.maximum}", cursor_position=len(text)
60
+ )
61
+
62
+ except ValueError:
63
+ raise ValidationError(message=f"Invalid {self.field_type}", cursor_position=len(text))
64
+
65
+
66
+ class SimpleStringValidator(Validator):
67
+ """Simple string validator with real-time feedback."""
68
+
69
+ def __init__(self, min_length: Optional[int] = None, max_length: Optional[int] = None):
70
+ self.min_length = min_length
71
+ self.max_length = max_length
72
+
73
+ def validate(self, document):
74
+ text = document.text
75
+ if not text:
76
+ return # Empty is OK for optional fields
77
+
78
+ if self.min_length is not None and len(text) < self.min_length:
79
+ raise ValidationError(
80
+ message=f"Need {self.min_length - len(text)} more chars", cursor_position=len(text)
81
+ )
82
+
83
+ if self.max_length is not None and len(text) > self.max_length:
84
+ raise ValidationError(
85
+ message=f"Too long by {len(text) - self.max_length} chars",
86
+ cursor_position=self.max_length,
87
+ )
88
+
89
+
90
+ class FormatValidator(Validator):
91
+ """Format-specific validator using Pydantic validators."""
92
+
93
+ def __init__(self, format_type: str):
94
+ self.format_type = format_type
95
+
96
+ def validate(self, document):
97
+ text = document.text.strip()
98
+ if not text:
99
+ return # Empty is OK for optional fields
100
+
101
+ try:
102
+ if self.format_type == "email":
103
+ # Use Pydantic model validation for email
104
+ from pydantic import BaseModel
105
+
106
+ class EmailModel(BaseModel):
107
+ email: EmailStr
108
+
109
+ EmailModel(email=text)
110
+ elif self.format_type == "uri":
111
+ # Use Pydantic model validation for URI
112
+ from pydantic import BaseModel
113
+
114
+ class UriModel(BaseModel):
115
+ uri: AnyUrl
116
+
117
+ UriModel(uri=text)
118
+ elif self.format_type == "date":
119
+ # Validate ISO date format (YYYY-MM-DD)
120
+ date.fromisoformat(text)
121
+ elif self.format_type == "date-time":
122
+ # Validate ISO datetime format
123
+ datetime.fromisoformat(text.replace("Z", "+00:00"))
124
+ except (PydanticValidationError, ValueError):
125
+ # Extract readable error message
126
+ if self.format_type == "email":
127
+ message = "Invalid email format"
128
+ elif self.format_type == "uri":
129
+ message = "Invalid URI format"
130
+ elif self.format_type == "date":
131
+ message = "Invalid date (use YYYY-MM-DD)"
132
+ elif self.format_type == "date-time":
133
+ message = "Invalid datetime (use ISO 8601)"
134
+ else:
135
+ message = f"Invalid {self.format_type} format"
136
+
137
+ raise ValidationError(message=message, cursor_position=len(text))
138
+
139
+
140
+ class ElicitationForm:
141
+ """Simplified elicitation form with all fields visible."""
142
+
143
+ def __init__(
144
+ self, schema: ElicitRequestedSchema, message: str, agent_name: str, server_name: str
145
+ ):
146
+ self.schema = schema
147
+ self.message = message
148
+ self.agent_name = agent_name
149
+ self.server_name = server_name
150
+
151
+ # Parse schema
152
+ self.properties = schema.get("properties", {})
153
+ self.required_fields = schema.get("required", [])
154
+
155
+ # Field storage
156
+ self.field_widgets = {}
157
+ self.multiline_fields = set() # Track which fields are multiline
158
+
159
+ # Result
160
+ self.result = None
161
+ self.action = "cancel"
162
+
163
+ # Build form
164
+ self._build_form()
165
+
166
+ def _build_form(self):
167
+ """Build the form layout."""
168
+
169
+ # Fast-agent provided data (Agent and MCP Server) - aligned labels
170
+ fastagent_info = FormattedText(
171
+ [
172
+ ("class:label", "Agent: "),
173
+ ("class:agent-name", self.agent_name),
174
+ ("class:label", "\nMCP Server: "),
175
+ ("class:server-name", self.server_name),
176
+ ]
177
+ )
178
+ fastagent_header = Window(
179
+ FormattedTextControl(fastagent_info),
180
+ height=2, # Just agent and server lines
181
+ )
182
+
183
+ # MCP Server provided message
184
+ mcp_message = FormattedText([("class:message", self.message)])
185
+ mcp_header = Window(
186
+ FormattedTextControl(mcp_message),
187
+ height=len(self.message.split("\n")),
188
+ )
189
+
190
+ # Create form fields - removed useless horizontal divider
191
+ form_fields = [
192
+ fastagent_header, # Fast-agent info
193
+ Window(height=1), # Spacing
194
+ mcp_header, # MCP server message
195
+ Window(height=1), # Spacing
196
+ ]
197
+
198
+ for field_name, field_def in self.properties.items():
199
+ field_widget = self._create_field(field_name, field_def)
200
+ if field_widget:
201
+ form_fields.append(field_widget)
202
+ form_fields.append(Window(height=1)) # Spacing
203
+
204
+ # Status line for error display (disabled ValidationToolbar to avoid confusion)
205
+ self.status_control = FormattedTextControl(text="")
206
+ self.status_line = Window(
207
+ self.status_control, height=1
208
+ ) # Store reference for later clearing
209
+
210
+ # Buttons - ensure they accept focus
211
+ submit_btn = Button("Accept", handler=self._accept)
212
+ cancel_btn = Button("Cancel", handler=self._cancel)
213
+ decline_btn = Button("Decline", handler=self._decline)
214
+ cancel_all_btn = Button("Cancel All", handler=self._cancel_all)
215
+
216
+ # Store button references for focus debugging
217
+ self.buttons = [submit_btn, decline_btn, cancel_btn, cancel_all_btn]
218
+
219
+ buttons = VSplit(
220
+ [
221
+ submit_btn,
222
+ Window(width=2),
223
+ decline_btn,
224
+ Window(width=2),
225
+ cancel_btn,
226
+ Window(width=2),
227
+ cancel_all_btn,
228
+ ]
229
+ )
230
+
231
+ # Main layout
232
+ form_fields.extend([self.status_line, buttons])
233
+ content = HSplit(form_fields)
234
+
235
+ # Add padding around content using HSplit and VSplit with empty windows
236
+ padded_content = HSplit(
237
+ [
238
+ Window(height=1), # Top padding
239
+ VSplit(
240
+ [
241
+ Window(width=2), # Left padding
242
+ content,
243
+ Window(width=2), # Right padding
244
+ ]
245
+ ),
246
+ Window(height=1), # Bottom padding
247
+ ]
248
+ )
249
+
250
+ # Wrap content in ScrollablePane to handle oversized forms
251
+ scrollable_content = ScrollablePane(
252
+ content=padded_content,
253
+ show_scrollbar=False, # Only show when content exceeds available space
254
+ display_arrows=False, # Only show when content exceeds available space
255
+ keep_cursor_visible=True,
256
+ keep_focused_window_visible=True,
257
+ )
258
+
259
+ # Dialog - formatted title with better styling and text
260
+ dialog = Dialog(
261
+ title=FormattedText([("class:title", "Elicitation Request")]),
262
+ body=scrollable_content,
263
+ with_background=True, # Re-enable background for proper layout
264
+ )
265
+
266
+ # Key bindings
267
+ kb = KeyBindings()
268
+
269
+ @kb.add("tab")
270
+ def focus_next_with_refresh(event):
271
+ focus_next(event)
272
+
273
+ @kb.add("s-tab")
274
+ def focus_previous_with_refresh(event):
275
+ focus_previous(event)
276
+
277
+ # Arrow key navigation - let radio lists handle up/down first
278
+ @kb.add("down")
279
+ def focus_next_arrow(event):
280
+ focus_next(event)
281
+
282
+ @kb.add("up")
283
+ def focus_previous_arrow(event):
284
+ focus_previous(event)
285
+
286
+ @kb.add("right", eager=True)
287
+ def focus_next_right(event):
288
+ focus_next(event)
289
+
290
+ @kb.add("left", eager=True)
291
+ def focus_previous_left(event):
292
+ focus_previous(event)
293
+
294
+ # Create filter for non-multiline fields
295
+ not_in_multiline = Condition(lambda: not self._is_in_multiline_field())
296
+
297
+ @kb.add("c-m", filter=not_in_multiline) # Enter to submit only when not in multiline
298
+ def submit(event):
299
+ self._accept()
300
+
301
+ @kb.add("c-j") # Ctrl+J as alternative submit for multiline fields
302
+ def submit_alt(event):
303
+ self._accept()
304
+
305
+ @kb.add("escape")
306
+ def cancel(event):
307
+ self._cancel()
308
+
309
+ # Create a root layout with the dialog and bottom toolbar
310
+ def get_toolbar():
311
+ # When clearing, return empty to hide the toolbar completely
312
+ if hasattr(self, "_toolbar_hidden") and self._toolbar_hidden:
313
+ return FormattedText([])
314
+
315
+ return FormattedText(
316
+ [
317
+ (
318
+ "class:bottom-toolbar.text",
319
+ " <TAB> or ↑↓→← to navigate. <ENTER> submit (<Ctrl+J> in multiline). <ESC> to cancel. ",
320
+ ),
321
+ (
322
+ "class:bottom-toolbar.text",
323
+ "<Cancel All> Auto-Cancel further elicitations from this Server.",
324
+ ),
325
+ ]
326
+ )
327
+
328
+ # Store toolbar function reference for later control
329
+ self._get_toolbar = get_toolbar
330
+ self._dialog = dialog
331
+
332
+ # Create toolbar window that we can reference later
333
+ self._toolbar_window = Window(
334
+ FormattedTextControl(get_toolbar), height=1, style="class:bottom-toolbar"
335
+ )
336
+
337
+ # Add toolbar to the layout
338
+ root_layout = HSplit(
339
+ [
340
+ dialog, # The main dialog
341
+ self._toolbar_window,
342
+ ]
343
+ )
344
+ self._root_layout = root_layout
345
+
346
+ # Application with toolbar and validation - ensure our styles override defaults
347
+ self.app = Application(
348
+ layout=Layout(root_layout),
349
+ key_bindings=kb,
350
+ full_screen=False, # Back to windowed mode for better integration
351
+ mouse_support=False,
352
+ style=ELICITATION_STYLE,
353
+ include_default_pygments_style=False, # Use only our custom style
354
+ )
355
+
356
+ # Set initial focus to first form field
357
+ def set_initial_focus():
358
+ try:
359
+ # Find first form field to focus on
360
+ first_field = None
361
+ for field_name in self.properties.keys():
362
+ widget = self.field_widgets.get(field_name)
363
+ if widget:
364
+ first_field = widget
365
+ break
366
+
367
+ if first_field:
368
+ self.app.layout.focus(first_field)
369
+ else:
370
+ # Fallback to first button if no fields
371
+ self.app.layout.focus(submit_btn)
372
+ except Exception:
373
+ pass # If focus fails, continue without it
374
+
375
+ # Schedule focus setting for after layout is ready
376
+ self.app.invalidate() # Ensure layout is built
377
+ set_initial_focus()
378
+
379
+ def _extract_string_constraints(self, field_def: Dict[str, Any]) -> Dict[str, Any]:
380
+ """Extract string constraints from field definition, handling anyOf schemas."""
381
+ constraints = {}
382
+
383
+ # Check direct constraints
384
+ if field_def.get("minLength") is not None:
385
+ constraints["minLength"] = field_def["minLength"]
386
+ if field_def.get("maxLength") is not None:
387
+ constraints["maxLength"] = field_def["maxLength"]
388
+
389
+ # Check anyOf constraints (for Optional fields)
390
+ if "anyOf" in field_def:
391
+ for variant in field_def["anyOf"]:
392
+ if variant.get("type") == "string":
393
+ if variant.get("minLength") is not None:
394
+ constraints["minLength"] = variant["minLength"]
395
+ if variant.get("maxLength") is not None:
396
+ constraints["maxLength"] = variant["maxLength"]
397
+ break
398
+
399
+ return constraints
400
+
401
+ def _create_field(self, field_name: str, field_def: Dict[str, Any]):
402
+ """Create a field widget."""
403
+
404
+ field_type = field_def.get("type", "string")
405
+ title = field_def.get("title", field_name)
406
+ description = field_def.get("description", "")
407
+ is_required = field_name in self.required_fields
408
+
409
+ # Build label with validation hints
410
+ label_text = title
411
+ if is_required:
412
+ label_text += " *"
413
+ if description:
414
+ label_text += f" - {description}"
415
+
416
+ # Add validation hints (simple ones stay on same line)
417
+ hints = []
418
+ format_hint = None
419
+
420
+ if field_type == "string":
421
+ constraints = self._extract_string_constraints(field_def)
422
+ if constraints.get("minLength"):
423
+ hints.append(f"min {constraints['minLength']} chars")
424
+ if constraints.get("maxLength"):
425
+ hints.append(f"max {constraints['maxLength']} chars")
426
+
427
+ # Handle format hints separately (these go on next line)
428
+ format_type = field_def.get("format")
429
+ if format_type:
430
+ format_info = {
431
+ "email": ("Email", "user@example.com"),
432
+ "uri": ("URI", "https://example.com"),
433
+ "date": ("Date", "YYYY-MM-DD"),
434
+ "date-time": ("Date Time", "YYYY-MM-DD HH:MM:SS"),
435
+ }
436
+ if format_type in format_info:
437
+ friendly_name, example = format_info[format_type]
438
+ format_hint = f"{friendly_name}: {example}"
439
+ else:
440
+ format_hint = format_type
441
+
442
+ elif field_type in ["number", "integer"]:
443
+ if field_def.get("minimum") is not None:
444
+ hints.append(f"min {field_def['minimum']}")
445
+ if field_def.get("maximum") is not None:
446
+ hints.append(f"max {field_def['maximum']}")
447
+ elif field_type == "string" and "enum" in field_def:
448
+ enum_names = field_def.get("enumNames", field_def["enum"])
449
+ hints.append(f"choose from: {', '.join(enum_names)}")
450
+
451
+ # Add simple hints to main label line
452
+ if hints:
453
+ label_text += f" ({', '.join(hints)})"
454
+
455
+ # Create multiline label if we have format hints
456
+ if format_hint:
457
+ label_lines = [label_text, f" → {format_hint}"]
458
+ label = Label(text="\n".join(label_lines))
459
+ else:
460
+ label = Label(text=label_text)
461
+
462
+ # Create input widget based on type
463
+ if field_type == "boolean":
464
+ default = field_def.get("default", False)
465
+ checkbox = Checkbox(text="Yes")
466
+ checkbox.checked = default
467
+ self.field_widgets[field_name] = checkbox
468
+
469
+ return HSplit([label, Frame(checkbox)])
470
+
471
+ elif field_type == "string" and "enum" in field_def:
472
+ enum_values = field_def["enum"]
473
+ enum_names = field_def.get("enumNames", enum_values)
474
+ values = [(val, name) for val, name in zip(enum_values, enum_names)]
475
+
476
+ radio_list = RadioList(values=values)
477
+ self.field_widgets[field_name] = radio_list
478
+
479
+ return HSplit([label, Frame(radio_list, height=min(len(values) + 2, 6))])
480
+
481
+ else:
482
+ # Text/number input
483
+ validator = None
484
+
485
+ if field_type in ["number", "integer"]:
486
+ validator = SimpleNumberValidator(
487
+ field_type=field_type,
488
+ minimum=field_def.get("minimum"),
489
+ maximum=field_def.get("maximum"),
490
+ )
491
+ elif field_type == "string":
492
+ constraints = self._extract_string_constraints(field_def)
493
+ format_type = field_def.get("format")
494
+
495
+ if format_type in ["email", "uri", "date", "date-time"]:
496
+ # Use format validator for specific formats
497
+ validator = FormatValidator(format_type)
498
+ else:
499
+ # Use string length validator for regular strings
500
+ validator = SimpleStringValidator(
501
+ min_length=constraints.get("minLength"),
502
+ max_length=constraints.get("maxLength"),
503
+ )
504
+ else:
505
+ constraints = {}
506
+
507
+ # Determine if field should be multiline based on max_length
508
+ if field_type == "string":
509
+ max_length = constraints.get("maxLength")
510
+ else:
511
+ max_length = None
512
+ if max_length and max_length > 100:
513
+ # Use multiline for longer fields
514
+ multiline = True
515
+ self.multiline_fields.add(field_name) # Track multiline fields
516
+ if max_length <= 300:
517
+ height = 3
518
+ else:
519
+ height = 5
520
+ else:
521
+ # Single line for shorter fields
522
+ multiline = False
523
+ height = 1
524
+
525
+ buffer = Buffer(
526
+ validator=validator,
527
+ multiline=multiline,
528
+ validate_while_typing=True, # Enable real-time validation
529
+ complete_while_typing=False, # Disable completion for cleaner experience
530
+ enable_history_search=False, # Disable history for cleaner experience
531
+ )
532
+ self.field_widgets[field_name] = buffer
533
+
534
+ # Create dynamic style function for focus highlighting and validation errors
535
+ def get_field_style():
536
+ """Dynamic style that changes based on focus and validation state."""
537
+ from prompt_toolkit.application.current import get_app
538
+
539
+ # Check if buffer has validation errors
540
+ if buffer.validation_error:
541
+ return "class:input-field.error"
542
+ elif get_app().layout.has_focus(buffer):
543
+ return "class:input-field.focused"
544
+ else:
545
+ return "class:input-field"
546
+
547
+ text_input = Window(
548
+ BufferControl(buffer=buffer),
549
+ height=height,
550
+ style=get_field_style, # Use dynamic style function
551
+ wrap_lines=True if multiline else False, # Enable word wrap for multiline
552
+ )
553
+
554
+ return HSplit([label, Frame(text_input)])
555
+
556
+ def _is_in_multiline_field(self) -> bool:
557
+ """Check if currently focused field is a multiline field."""
558
+ from prompt_toolkit.application.current import get_app
559
+
560
+ focused = get_app().layout.current_control
561
+
562
+ # Find which field this control belongs to
563
+ # Only Buffer widgets can be multiline, so only check those
564
+ for field_name, widget in self.field_widgets.items():
565
+ if (
566
+ isinstance(widget, Buffer)
567
+ and hasattr(focused, "buffer")
568
+ and widget == focused.buffer
569
+ ):
570
+ return field_name in self.multiline_fields
571
+ return False
572
+
573
+ def _validate_form(self) -> tuple[bool, Optional[str]]:
574
+ """Validate the entire form."""
575
+
576
+ # First, check all fields for validation errors from their validators
577
+ for field_name, field_def in self.properties.items():
578
+ widget = self.field_widgets.get(field_name)
579
+ if widget is None:
580
+ continue
581
+
582
+ # Check for validation errors from validators
583
+ if isinstance(widget, Buffer):
584
+ if widget.validation_error:
585
+ title = field_def.get("title", field_name)
586
+ return False, f"'{title}': {widget.validation_error.message}"
587
+
588
+ # Then check if required fields are empty
589
+ for field_name in self.required_fields:
590
+ widget = self.field_widgets.get(field_name)
591
+ if widget is None:
592
+ continue
593
+
594
+ # Check if required field has value
595
+ if isinstance(widget, Buffer):
596
+ if not widget.text.strip():
597
+ title = self.properties[field_name].get("title", field_name)
598
+ return False, f"'{title}' is required"
599
+ elif isinstance(widget, RadioList):
600
+ if widget.current_value is None:
601
+ title = self.properties[field_name].get("title", field_name)
602
+ return False, f"'{title}' is required"
603
+
604
+ return True, None
605
+
606
+ def _get_form_data(self) -> Dict[str, Any]:
607
+ """Extract data from form fields."""
608
+ data = {}
609
+
610
+ for field_name, field_def in self.properties.items():
611
+ widget = self.field_widgets.get(field_name)
612
+ if widget is None:
613
+ continue
614
+
615
+ field_type = field_def.get("type", "string")
616
+
617
+ if isinstance(widget, Buffer):
618
+ value = widget.text.strip()
619
+ if value:
620
+ if field_type == "integer":
621
+ try:
622
+ data[field_name] = int(value)
623
+ except ValueError:
624
+ # This should not happen due to validation, but be safe
625
+ raise ValueError(f"Invalid integer value for {field_name}: {value}")
626
+ elif field_type == "number":
627
+ try:
628
+ data[field_name] = float(value)
629
+ except ValueError:
630
+ # This should not happen due to validation, but be safe
631
+ raise ValueError(f"Invalid number value for {field_name}: {value}")
632
+ else:
633
+ data[field_name] = value
634
+ elif field_name not in self.required_fields:
635
+ data[field_name] = None
636
+
637
+ elif isinstance(widget, Checkbox):
638
+ data[field_name] = widget.checked
639
+
640
+ elif isinstance(widget, RadioList):
641
+ if widget.current_value is not None:
642
+ data[field_name] = widget.current_value
643
+
644
+ return data
645
+
646
+ def _accept(self):
647
+ """Handle form submission."""
648
+ # Validate
649
+ is_valid, error_msg = self._validate_form()
650
+ if not is_valid:
651
+ # Use styled error message
652
+ self.status_control.text = FormattedText(
653
+ [("class:validation-error", f"Error: {error_msg}")]
654
+ )
655
+ return
656
+
657
+ # Get data
658
+ try:
659
+ self.result = self._get_form_data()
660
+ self.action = "accept"
661
+ self._clear_status_bar()
662
+ self.app.exit()
663
+ except Exception as e:
664
+ # Use styled error message
665
+ self.status_control.text = FormattedText(
666
+ [("class:validation-error", f"Error: {str(e)}")]
667
+ )
668
+
669
+ def _cancel(self):
670
+ """Handle cancel."""
671
+ self.action = "cancel"
672
+ self._clear_status_bar()
673
+ self.app.exit()
674
+
675
+ def _decline(self):
676
+ """Handle decline."""
677
+ self.action = "decline"
678
+ self._clear_status_bar()
679
+ self.app.exit()
680
+
681
+ def _cancel_all(self):
682
+ """Handle cancel all - cancels and disables future elicitations."""
683
+ elicitation_state.disable_server(self.server_name)
684
+ self.action = "disable"
685
+ self._clear_status_bar()
686
+ self.app.exit()
687
+
688
+ def _clear_status_bar(self):
689
+ """Hide the status bar by removing it from the layout."""
690
+ # Create completely clean layout - just empty space with application background
691
+ from prompt_toolkit.layout import HSplit, Window
692
+ from prompt_toolkit.layout.controls import FormattedTextControl
693
+
694
+ # Create a simple empty window with application background
695
+ empty_window = Window(
696
+ FormattedTextControl(FormattedText([("class:application", "")])), height=1
697
+ )
698
+
699
+ # Replace entire layout with just the empty window
700
+ new_layout = HSplit([empty_window])
701
+
702
+ # Update the app's layout
703
+ if hasattr(self, "app") and self.app:
704
+ self.app.layout.container = new_layout
705
+ self.app.invalidate()
706
+
707
+ async def run_async(self) -> tuple[str, Optional[Dict[str, Any]]]:
708
+ """Run the form and return result."""
709
+ try:
710
+ await self.app.run_async()
711
+ except Exception as e:
712
+ print(f"Form error: {e}")
713
+ self.action = "cancel"
714
+ self._clear_status_bar()
715
+ return self.action, self.result
716
+
717
+
718
+ async def show_simple_elicitation_form(
719
+ schema: ElicitRequestedSchema, message: str, agent_name: str, server_name: str
720
+ ) -> tuple[str, Optional[Dict[str, Any]]]:
721
+ """Show the simplified elicitation form."""
722
+ form = ElicitationForm(schema, message, agent_name, server_name)
723
+ return await form.run_async()