fast-agent-mcp 0.2.44__py3-none-any.whl → 0.2.46__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.

@@ -6,6 +6,7 @@ for creating agents in the DirectFastAgent framework.
6
6
 
7
7
  import inspect
8
8
  from functools import wraps
9
+ from pathlib import Path
9
10
  from typing import (
10
11
  Awaitable,
11
12
  Callable,
@@ -21,8 +22,12 @@ from typing import (
21
22
  )
22
23
 
23
24
  from mcp.client.session import ElicitationFnT
25
+ from pydantic import AnyUrl
24
26
 
25
27
  from mcp_agent.agents.agent import AgentConfig
28
+ from mcp_agent.agents.workflow.router_agent import (
29
+ ROUTING_SYSTEM_INSTRUCTION,
30
+ )
26
31
  from mcp_agent.core.agent_types import AgentType
27
32
  from mcp_agent.core.request_params import RequestParams
28
33
 
@@ -82,6 +87,91 @@ class DecoratedEvaluatorOptimizerProtocol(DecoratedAgentProtocol[P, R], Protocol
82
87
  _evaluator: str
83
88
 
84
89
 
90
+ def _fetch_url_content(url: str) -> str:
91
+ """
92
+ Fetch content from a URL.
93
+
94
+ Args:
95
+ url: The URL to fetch content from
96
+
97
+ Returns:
98
+ The text content from the URL
99
+
100
+ Raises:
101
+ requests.RequestException: If the URL cannot be fetched
102
+ UnicodeDecodeError: If the content cannot be decoded as UTF-8
103
+ """
104
+ import requests
105
+
106
+ response = requests.get(url, timeout=10)
107
+ response.raise_for_status() # Raise exception for HTTP errors
108
+ return response.text
109
+
110
+
111
+ def _apply_templates(text: str) -> str:
112
+ """
113
+ Apply template substitutions to instruction text.
114
+
115
+ Supported templates:
116
+ {{currentDate}} - Current date in format "24 July 2025"
117
+ {{url:https://...}} - Content fetched from the specified URL
118
+
119
+ Args:
120
+ text: The text to process
121
+
122
+ Returns:
123
+ Text with template substitutions applied
124
+
125
+ Raises:
126
+ requests.RequestException: If a URL in {{url:...}} cannot be fetched
127
+ UnicodeDecodeError: If URL content cannot be decoded as UTF-8
128
+ """
129
+ import re
130
+ from datetime import datetime
131
+
132
+ # Apply {{currentDate}} template
133
+ current_date = datetime.now().strftime("%d %B %Y")
134
+ text = text.replace("{{currentDate}}", current_date)
135
+
136
+ # Apply {{url:...}} templates
137
+ url_pattern = re.compile(r"\{\{url:(https?://[^}]+)\}\}")
138
+
139
+ def replace_url(match):
140
+ url = match.group(1)
141
+ return _fetch_url_content(url)
142
+
143
+ text = url_pattern.sub(replace_url, text)
144
+
145
+ return text
146
+
147
+
148
+ def _resolve_instruction(instruction: str | Path | AnyUrl) -> str:
149
+ """
150
+ Resolve instruction from either a string, Path, or URL with template support.
151
+
152
+ Args:
153
+ instruction: Either a string instruction, Path to a file, or URL containing the instruction
154
+
155
+ Returns:
156
+ The resolved instruction string with templates applied
157
+
158
+ Raises:
159
+ FileNotFoundError: If the Path doesn't exist
160
+ PermissionError: If the Path can't be read
161
+ UnicodeDecodeError: If the file/URL content can't be decoded as UTF-8
162
+ requests.RequestException: If the URL cannot be fetched
163
+ """
164
+ if isinstance(instruction, Path):
165
+ text = instruction.read_text(encoding="utf-8")
166
+ elif isinstance(instruction, AnyUrl):
167
+ text = _fetch_url_content(str(instruction))
168
+ else:
169
+ text = instruction
170
+
171
+ # Apply template substitutions
172
+ return _apply_templates(text)
173
+
174
+
85
175
  def _decorator_impl(
86
176
  self,
87
177
  agent_type: AgentType,
@@ -180,9 +270,9 @@ def _decorator_impl(
180
270
  def agent(
181
271
  self,
182
272
  name: str = "default",
183
- instruction_or_kwarg: Optional[str] = None,
273
+ instruction_or_kwarg: Optional[str | Path | AnyUrl] = None,
184
274
  *,
185
- instruction: str = "You are a helpful agent.",
275
+ instruction: str | Path | AnyUrl = "You are a helpful agent.",
186
276
  servers: List[str] = [],
187
277
  tools: Optional[Dict[str, List[str]]] = None,
188
278
  resources: Optional[Dict[str, List[str]]] = None,
@@ -217,7 +307,10 @@ def agent(
217
307
  Returns:
218
308
  A decorator that registers the agent with proper type annotations
219
309
  """
220
- final_instruction = instruction_or_kwarg if instruction_or_kwarg is not None else instruction
310
+ final_instruction_raw = (
311
+ instruction_or_kwarg if instruction_or_kwarg is not None else instruction
312
+ )
313
+ final_instruction = _resolve_instruction(final_instruction_raw)
221
314
 
222
315
  return _decorator_impl(
223
316
  self,
@@ -242,9 +335,9 @@ def custom(
242
335
  self,
243
336
  cls,
244
337
  name: str = "default",
245
- instruction_or_kwarg: Optional[str] = None,
338
+ instruction_or_kwarg: Optional[str | Path | AnyUrl] = None,
246
339
  *,
247
- instruction: str = "You are a helpful agent.",
340
+ instruction: str | Path | AnyUrl = "You are a helpful agent.",
248
341
  servers: List[str] = [],
249
342
  tools: Optional[Dict[str, List[str]]] = None,
250
343
  resources: Optional[Dict[str, List[str]]] = None,
@@ -274,7 +367,10 @@ def custom(
274
367
  Returns:
275
368
  A decorator that registers the agent with proper type annotations
276
369
  """
277
- final_instruction = instruction_or_kwarg if instruction_or_kwarg is not None else instruction
370
+ final_instruction_raw = (
371
+ instruction_or_kwarg if instruction_or_kwarg is not None else instruction
372
+ )
373
+ final_instruction = _resolve_instruction(final_instruction_raw)
278
374
 
279
375
  return _decorator_impl(
280
376
  self,
@@ -308,7 +404,7 @@ def orchestrator(
308
404
  name: str,
309
405
  *,
310
406
  agents: List[str],
311
- instruction: str = DEFAULT_INSTRUCTION_ORCHESTRATOR,
407
+ instruction: str | Path | AnyUrl = DEFAULT_INSTRUCTION_ORCHESTRATOR,
312
408
  model: Optional[str] = None,
313
409
  request_params: RequestParams | None = None,
314
410
  use_history: bool = False,
@@ -338,6 +434,7 @@ def orchestrator(
338
434
  """
339
435
 
340
436
  # Create final request params with plan_iterations
437
+ resolved_instruction = _resolve_instruction(instruction)
341
438
 
342
439
  return cast(
343
440
  "Callable[[AgentCallable[P, R]], DecoratedOrchestratorProtocol[P, R]]",
@@ -345,7 +442,7 @@ def orchestrator(
345
442
  self,
346
443
  AgentType.ORCHESTRATOR,
347
444
  name=name,
348
- instruction=instruction,
445
+ instruction=resolved_instruction,
349
446
  servers=[], # Orchestrators don't connect to servers directly
350
447
  model=model,
351
448
  use_history=use_history,
@@ -365,7 +462,7 @@ def router(
365
462
  name: str,
366
463
  *,
367
464
  agents: List[str],
368
- instruction: Optional[str] = None,
465
+ instruction: Optional[str | Path | AnyUrl] = None,
369
466
  servers: List[str] = [],
370
467
  tools: Optional[Dict[str, List[str]]] = None,
371
468
  resources: Optional[Dict[str, List[str]]] = None,
@@ -397,10 +494,7 @@ def router(
397
494
  Returns:
398
495
  A decorator that registers the router with proper type annotations
399
496
  """
400
- default_instruction = """
401
- You are a router that determines which specialized agent should handle a given query.
402
- Analyze the query and select the most appropriate agent to handle it.
403
- """
497
+ resolved_instruction = _resolve_instruction(instruction or ROUTING_SYSTEM_INSTRUCTION)
404
498
 
405
499
  return cast(
406
500
  "Callable[[AgentCallable[P, R]], DecoratedRouterProtocol[P, R]]",
@@ -408,7 +502,7 @@ def router(
408
502
  self,
409
503
  AgentType.ROUTER,
410
504
  name=name,
411
- instruction=instruction or default_instruction,
505
+ instruction=resolved_instruction,
412
506
  servers=servers,
413
507
  model=model,
414
508
  use_history=use_history,
@@ -430,7 +524,7 @@ def chain(
430
524
  name: str,
431
525
  *,
432
526
  sequence: List[str],
433
- instruction: Optional[str] = None,
527
+ instruction: Optional[str | Path | AnyUrl] = None,
434
528
  cumulative: bool = False,
435
529
  default: bool = False,
436
530
  ) -> Callable[[AgentCallable[P, R]], DecoratedChainProtocol[P, R]]:
@@ -457,6 +551,7 @@ def chain(
457
551
  You are a chain that processes requests through a series of specialized agents in sequence.
458
552
  Pass the output of each agent to the next agent in the chain.
459
553
  """
554
+ resolved_instruction = _resolve_instruction(instruction or default_instruction)
460
555
 
461
556
  return cast(
462
557
  "Callable[[AgentCallable[P, R]], DecoratedChainProtocol[P, R]]",
@@ -464,7 +559,7 @@ def chain(
464
559
  self,
465
560
  AgentType.CHAIN,
466
561
  name=name,
467
- instruction=instruction or default_instruction,
562
+ instruction=resolved_instruction,
468
563
  sequence=sequence,
469
564
  cumulative=cumulative,
470
565
  default=default,
@@ -478,7 +573,7 @@ def parallel(
478
573
  *,
479
574
  fan_out: List[str],
480
575
  fan_in: str | None = None,
481
- instruction: Optional[str] = None,
576
+ instruction: Optional[str | Path | AnyUrl] = None,
482
577
  include_request: bool = True,
483
578
  default: bool = False,
484
579
  ) -> Callable[[AgentCallable[P, R]], DecoratedParallelProtocol[P, R]]:
@@ -500,6 +595,7 @@ def parallel(
500
595
  You are a parallel processor that executes multiple agents simultaneously
501
596
  and aggregates their results.
502
597
  """
598
+ resolved_instruction = _resolve_instruction(instruction or default_instruction)
503
599
 
504
600
  return cast(
505
601
  "Callable[[AgentCallable[P, R]], DecoratedParallelProtocol[P, R]]",
@@ -507,7 +603,7 @@ def parallel(
507
603
  self,
508
604
  AgentType.PARALLEL,
509
605
  name=name,
510
- instruction=instruction or default_instruction,
606
+ instruction=resolved_instruction,
511
607
  servers=[], # Parallel agents don't connect to servers directly
512
608
  fan_in=fan_in,
513
609
  fan_out=fan_out,
@@ -523,7 +619,7 @@ def evaluator_optimizer(
523
619
  *,
524
620
  generator: str,
525
621
  evaluator: str,
526
- instruction: Optional[str] = None,
622
+ instruction: Optional[str | Path | AnyUrl] = None,
527
623
  min_rating: str = "GOOD",
528
624
  max_refinements: int = 3,
529
625
  default: bool = False,
@@ -548,6 +644,7 @@ def evaluator_optimizer(
548
644
  evaluated for quality, and then refined based on specific feedback until
549
645
  it reaches an acceptable quality standard.
550
646
  """
647
+ resolved_instruction = _resolve_instruction(instruction or default_instruction)
551
648
 
552
649
  return cast(
553
650
  "Callable[[AgentCallable[P, R]], DecoratedEvaluatorOptimizerProtocol[P, R]]",
@@ -555,7 +652,7 @@ def evaluator_optimizer(
555
652
  self,
556
653
  AgentType.EVALUATOR_OPTIMIZER,
557
654
  name=name,
558
- instruction=instruction or default_instruction,
655
+ instruction=resolved_instruction,
559
656
  servers=[], # Evaluator-optimizer doesn't connect to servers directly
560
657
  generator=generator,
561
658
  evaluator=evaluator,
@@ -511,13 +511,12 @@ def create_keybindings(on_toggle_multiline=None, app=None, agent_provider=None,
511
511
  rich_print("\n[green]✓ Copied to clipboard[/green]")
512
512
  return
513
513
 
514
- rich_print("\n[yellow]No assistant messages found[/yellow]")
515
514
  else:
516
- rich_print("\n[yellow]No message history available[/yellow]")
517
- except Exception as e:
518
- rich_print(f"\n[red]Error copying: {e}[/red]")
515
+ pass
516
+ except Exception:
517
+ pass
519
518
  else:
520
- rich_print("[yellow]Clipboard copy not available in this context[/yellow]")
519
+ pass
521
520
 
522
521
  return kb
523
522
 
@@ -0,0 +1,50 @@
1
+ """Human input modules for forms and elicitation."""
2
+
3
+ # Export the simple form API
4
+ # Export field types and schema builder
5
+ from mcp_agent.human_input.form_fields import (
6
+ BooleanField,
7
+ EnumField,
8
+ FormSchema,
9
+ IntegerField,
10
+ NumberField,
11
+ # Field classes
12
+ StringField,
13
+ boolean,
14
+ choice,
15
+ date,
16
+ datetime,
17
+ email,
18
+ integer,
19
+ number,
20
+ # Convenience functions
21
+ string,
22
+ url,
23
+ )
24
+ from mcp_agent.human_input.simple_form import ask, ask_sync, form, form_sync
25
+
26
+ __all__ = [
27
+ # Form functions
28
+ "form",
29
+ "form_sync",
30
+ "ask",
31
+ "ask_sync",
32
+ # Schema builder
33
+ "FormSchema",
34
+ # Field classes
35
+ "StringField",
36
+ "IntegerField",
37
+ "NumberField",
38
+ "BooleanField",
39
+ "EnumField",
40
+ # Field convenience functions
41
+ "string",
42
+ "email",
43
+ "url",
44
+ "date",
45
+ "datetime",
46
+ "integer",
47
+ "number",
48
+ "boolean",
49
+ "choice",
50
+ ]
@@ -5,6 +5,7 @@ from typing import Any, Dict, Optional
5
5
 
6
6
  from mcp.types import ElicitRequestedSchema
7
7
  from prompt_toolkit import Application
8
+ from prompt_toolkit.application.current import get_app
8
9
  from prompt_toolkit.buffer import Buffer
9
10
  from prompt_toolkit.filters import Condition
10
11
  from prompt_toolkit.formatted_text import FormattedText
@@ -272,29 +273,32 @@ class ElicitationForm:
272
273
  keep_focused_window_visible=True,
273
274
  )
274
275
 
275
- # Create title bar manually
276
- title_bar = Window(
277
- FormattedTextControl(FormattedText([("class:title", "Elicitation Request")])),
278
- height=1,
279
- style="class:dialog.title",
280
- )
281
-
282
- # Combine title, sticky headers, and scrollable content
276
+ # Combine sticky headers and scrollable content (no separate title bar needed)
283
277
  full_content = HSplit(
284
278
  [
285
- title_bar,
286
- Window(height=1), # Spacing after title
279
+ Window(height=1), # Top spacing
287
280
  sticky_headers, # Headers stay fixed at top
288
281
  scrollable_content, # Form fields can scroll
289
282
  ]
290
283
  )
291
284
 
292
- # Create dialog frame manually to avoid Dialog's internal scrolling
285
+ # Create dialog frame with title
293
286
  dialog = Frame(
294
287
  body=full_content,
288
+ title="Elicitation Request",
295
289
  style="class:dialog",
296
290
  )
297
291
 
292
+ # Apply width constraints by putting Frame in VSplit with flexible spacers
293
+ # This prevents console display interference and constrains the Frame border
294
+ constrained_dialog = VSplit(
295
+ [
296
+ Window(width=10), # Smaller left spacer
297
+ dialog,
298
+ Window(width=10), # Smaller right spacer
299
+ ]
300
+ )
301
+
298
302
  # Key bindings
299
303
  kb = KeyBindings()
300
304
 
@@ -370,7 +374,7 @@ class ElicitationForm:
370
374
  # Add toolbar to the layout
371
375
  root_layout = HSplit(
372
376
  [
373
- dialog, # The main dialog
377
+ constrained_dialog, # The width-constrained dialog
374
378
  self._toolbar_window,
375
379
  ]
376
380
  )
@@ -588,7 +592,6 @@ class ElicitationForm:
588
592
 
589
593
  def _is_in_multiline_field(self) -> bool:
590
594
  """Check if currently focused field is a multiline field."""
591
- from prompt_toolkit.application.current import get_app
592
595
 
593
596
  focused = get_app().layout.current_control
594
597
 
@@ -0,0 +1,252 @@
1
+ """High-level field types for elicitation forms with default support."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Dict, List, Optional, Union
5
+
6
+
7
+ @dataclass
8
+ class StringField:
9
+ """String field with validation and default support."""
10
+
11
+ title: Optional[str] = None
12
+ description: Optional[str] = None
13
+ default: Optional[str] = None
14
+ min_length: Optional[int] = None
15
+ max_length: Optional[int] = None
16
+ format: Optional[str] = None # email, uri, date, date-time
17
+
18
+ def to_schema(self) -> Dict[str, Any]:
19
+ """Convert to MCP elicitation schema format."""
20
+ schema: Dict[str, Any] = {"type": "string"}
21
+
22
+ if self.title:
23
+ schema["title"] = self.title
24
+ if self.description:
25
+ schema["description"] = self.description
26
+ if self.default is not None:
27
+ schema["default"] = self.default
28
+ if self.min_length is not None:
29
+ schema["minLength"] = self.min_length
30
+ if self.max_length is not None:
31
+ schema["maxLength"] = self.max_length
32
+ if self.format:
33
+ schema["format"] = self.format
34
+
35
+ return schema
36
+
37
+
38
+ @dataclass
39
+ class IntegerField:
40
+ """Integer field with validation and default support."""
41
+
42
+ title: Optional[str] = None
43
+ description: Optional[str] = None
44
+ default: Optional[int] = None
45
+ minimum: Optional[int] = None
46
+ maximum: Optional[int] = None
47
+
48
+ def to_schema(self) -> Dict[str, Any]:
49
+ """Convert to MCP elicitation schema format."""
50
+ schema: Dict[str, Any] = {"type": "integer"}
51
+
52
+ if self.title:
53
+ schema["title"] = self.title
54
+ if self.description:
55
+ schema["description"] = self.description
56
+ if self.default is not None:
57
+ schema["default"] = self.default
58
+ if self.minimum is not None:
59
+ schema["minimum"] = self.minimum
60
+ if self.maximum is not None:
61
+ schema["maximum"] = self.maximum
62
+
63
+ return schema
64
+
65
+
66
+ @dataclass
67
+ class NumberField:
68
+ """Number (float) field with validation and default support."""
69
+
70
+ title: Optional[str] = None
71
+ description: Optional[str] = None
72
+ default: Optional[float] = None
73
+ minimum: Optional[float] = None
74
+ maximum: Optional[float] = None
75
+
76
+ def to_schema(self) -> Dict[str, Any]:
77
+ """Convert to MCP elicitation schema format."""
78
+ schema: Dict[str, Any] = {"type": "number"}
79
+
80
+ if self.title:
81
+ schema["title"] = self.title
82
+ if self.description:
83
+ schema["description"] = self.description
84
+ if self.default is not None:
85
+ schema["default"] = self.default
86
+ if self.minimum is not None:
87
+ schema["minimum"] = self.minimum
88
+ if self.maximum is not None:
89
+ schema["maximum"] = self.maximum
90
+
91
+ return schema
92
+
93
+
94
+ @dataclass
95
+ class BooleanField:
96
+ """Boolean field with default support."""
97
+
98
+ title: Optional[str] = None
99
+ description: Optional[str] = None
100
+ default: Optional[bool] = None
101
+
102
+ def to_schema(self) -> Dict[str, Any]:
103
+ """Convert to MCP elicitation schema format."""
104
+ schema: Dict[str, Any] = {"type": "boolean"}
105
+
106
+ if self.title:
107
+ schema["title"] = self.title
108
+ if self.description:
109
+ schema["description"] = self.description
110
+ if self.default is not None:
111
+ schema["default"] = self.default
112
+
113
+ return schema
114
+
115
+
116
+ @dataclass
117
+ class EnumField:
118
+ """Enum/choice field with default support."""
119
+
120
+ choices: List[str]
121
+ choice_names: Optional[List[str]] = None # Human-readable names
122
+ title: Optional[str] = None
123
+ description: Optional[str] = None
124
+ default: Optional[str] = None
125
+
126
+ def to_schema(self) -> Dict[str, Any]:
127
+ """Convert to MCP elicitation schema format."""
128
+ schema: Dict[str, Any] = {"type": "string", "enum": self.choices}
129
+
130
+ if self.title:
131
+ schema["title"] = self.title
132
+ if self.description:
133
+ schema["description"] = self.description
134
+ if self.default is not None:
135
+ schema["default"] = self.default
136
+ if self.choice_names:
137
+ schema["enumNames"] = self.choice_names
138
+
139
+ return schema
140
+
141
+
142
+ # Field type union
143
+ FieldType = Union[StringField, IntegerField, NumberField, BooleanField, EnumField]
144
+
145
+
146
+ class FormSchema:
147
+ """High-level form schema builder."""
148
+
149
+ def __init__(self, **fields: FieldType):
150
+ """Create a form schema with named fields."""
151
+ self.fields = fields
152
+ self._required_fields: List[str] = []
153
+
154
+ def required(self, *field_names: str) -> "FormSchema":
155
+ """Mark fields as required."""
156
+ self._required_fields.extend(field_names)
157
+ return self
158
+
159
+ def to_schema(self) -> Dict[str, Any]:
160
+ """Convert to MCP ElicitRequestedSchema format."""
161
+ properties = {}
162
+
163
+ for field_name, field in self.fields.items():
164
+ properties[field_name] = field.to_schema()
165
+
166
+ schema: Dict[str, Any] = {"type": "object", "properties": properties}
167
+
168
+ if self._required_fields:
169
+ schema["required"] = self._required_fields
170
+
171
+ return schema
172
+
173
+
174
+ # Convenience functions for creating fields
175
+ def string(
176
+ title: Optional[str] = None,
177
+ description: Optional[str] = None,
178
+ default: Optional[str] = None,
179
+ min_length: Optional[int] = None,
180
+ max_length: Optional[int] = None,
181
+ format: Optional[str] = None,
182
+ ) -> StringField:
183
+ """Create a string field."""
184
+ return StringField(title, description, default, min_length, max_length, format)
185
+
186
+
187
+ def email(
188
+ title: Optional[str] = None, description: Optional[str] = None, default: Optional[str] = None
189
+ ) -> StringField:
190
+ """Create an email field."""
191
+ return StringField(title, description, default, format="email")
192
+
193
+
194
+ def url(
195
+ title: Optional[str] = None, description: Optional[str] = None, default: Optional[str] = None
196
+ ) -> StringField:
197
+ """Create a URL field."""
198
+ return StringField(title, description, default, format="uri")
199
+
200
+
201
+ def date(
202
+ title: Optional[str] = None, description: Optional[str] = None, default: Optional[str] = None
203
+ ) -> StringField:
204
+ """Create a date field."""
205
+ return StringField(title, description, default, format="date")
206
+
207
+
208
+ def datetime(
209
+ title: Optional[str] = None, description: Optional[str] = None, default: Optional[str] = None
210
+ ) -> StringField:
211
+ """Create a datetime field."""
212
+ return StringField(title, description, default, format="date-time")
213
+
214
+
215
+ def integer(
216
+ title: Optional[str] = None,
217
+ description: Optional[str] = None,
218
+ default: Optional[int] = None,
219
+ minimum: Optional[int] = None,
220
+ maximum: Optional[int] = None,
221
+ ) -> IntegerField:
222
+ """Create an integer field."""
223
+ return IntegerField(title, description, default, minimum, maximum)
224
+
225
+
226
+ def number(
227
+ title: Optional[str] = None,
228
+ description: Optional[str] = None,
229
+ default: Optional[float] = None,
230
+ minimum: Optional[float] = None,
231
+ maximum: Optional[float] = None,
232
+ ) -> NumberField:
233
+ """Create a number field."""
234
+ return NumberField(title, description, default, minimum, maximum)
235
+
236
+
237
+ def boolean(
238
+ title: Optional[str] = None, description: Optional[str] = None, default: Optional[bool] = None
239
+ ) -> BooleanField:
240
+ """Create a boolean field."""
241
+ return BooleanField(title, description, default)
242
+
243
+
244
+ def choice(
245
+ choices: List[str],
246
+ choice_names: Optional[List[str]] = None,
247
+ title: Optional[str] = None,
248
+ description: Optional[str] = None,
249
+ default: Optional[str] = None,
250
+ ) -> EnumField:
251
+ """Create a choice/enum field."""
252
+ return EnumField(choices, choice_names, title, description, default)