construct-labs-crm-env 0.1.2__py3-none-any.whl → 0.1.5__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.
@@ -35,6 +35,7 @@ from .models import (
35
35
  CrmAgentState,
36
36
  )
37
37
  from .protocol import ParsedAction
38
+ from .tools import DEFAULT_TOOLS
38
39
 
39
40
  # Type alias for JSON-serializable dictionaries
40
41
  JsonDict = dict[str, Any]
@@ -105,7 +106,7 @@ class CrmAgentEnv(EnvClient[CrmAgentAction, CrmAgentObservation, CrmAgentState])
105
106
  raise ValueError(
106
107
  "API key is required. Pass api_key parameter or set "
107
108
  "CRM_AGENT_API_KEY environment variable. "
108
- "Get your API key by contacting hello@construct-labs.com"
109
+ "Contact hello@construct-labs.com to obtain an API key."
109
110
  )
110
111
 
111
112
  self._api_key = resolved_api_key
@@ -162,7 +163,7 @@ class CrmAgentEnv(EnvClient[CrmAgentAction, CrmAgentObservation, CrmAgentState])
162
163
  if "401" in error_msg or "403" in error_msg or "4001" in error_msg:
163
164
  raise ConnectionError(
164
165
  "Authentication failed. Please verify your API key. "
165
- "Get a valid key at https://construct-labs.com/api-keys"
166
+ "Contact hello@construct-labs.com if you need assistance."
166
167
  ) from e
167
168
  raise ConnectionError(f"Failed to connect to {self._ws_url}: {e}") from e
168
169
  finally:
@@ -194,12 +195,6 @@ class CrmAgentEnv(EnvClient[CrmAgentAction, CrmAgentObservation, CrmAgentState])
194
195
  """Parse server response into CrmAgentState."""
195
196
  return CrmAgentState.model_validate(payload)
196
197
 
197
- def _reset_payload(self, seed: int | None = None) -> JsonDict:
198
- """Create payload for reset request."""
199
- if seed is not None:
200
- return {"seed": seed}
201
- return {}
202
-
203
198
  # =========================================================================
204
199
  # Extensible Properties - Override these in subclasses
205
200
  # =========================================================================
@@ -233,48 +228,95 @@ class CrmAgentEnv(EnvClient[CrmAgentAction, CrmAgentObservation, CrmAgentState])
233
228
  """
234
229
  return """You are a tool-using agent interacting with a CRM (Customer Relationship Management) system.
235
230
 
236
- GOAL: Complete CRM tasks by creating, updating, and managing business data.
231
+ ## GOAL
232
+
233
+ Complete CRM tasks by creating, updating, retrieving, and managing business data.
234
+
235
+ ## DATA MODEL
236
+
237
+ - Companies: Organizations you do business with
238
+ - People: Contacts who work at companies (linked via person_company_id)
239
+ - Opportunities: Sales deals linked to a company and a person (point of contact)
240
+ - Notes: Free-text records attached to any company, person, or opportunity
241
+ - Tasks: Action items with due dates, attached to any company, person, or opportunity
242
+
243
+ ## AVAILABLE TOOLS
244
+
245
+ - Companies: list_companies, get_company, create_company, update_company, delete_company
246
+ - People: list_people, get_person, create_person, update_person, delete_person
247
+ - Opportunities: list_opportunities, get_opportunity, create_opportunity, update_opportunity, delete_opportunity
248
+ - Notes: list_notes, create_note
249
+ - Tasks: list_tasks, create_task, update_task, complete_task
250
+ - Answer: submit_answer
251
+
252
+ ## FILTERING AND PAGINATION
253
+
254
+ Use the filter parameter to search records. Format: field[comparator]:value
255
+
256
+ Comparators: eq, neq, gt, gte, lt, lte, ilike (case-insensitive like), in, is, startsWith, containsAny
257
+
258
+ Examples:
259
+ - name[ilike]:"%acme%" - names containing "acme"
260
+ - stage[eq]:"WON" - opportunities with stage WON
261
+ - amount[gte]:10000 - deals worth $10,000 or more
262
+ - createdAt[gte]:"2026-01-01" - records created this year
263
+ - deletedAt[is]:NULL - non-deleted records
264
+
265
+ Rules: Quote strings and dates. Do not quote numbers. Combine with comma for AND: field1[eq]:"a",field2[gt]:5
266
+
267
+ Pagination: Results return max 60 records. Use starting_after with the endCursor from pageInfo to get more.
268
+
269
+ ## WORKFLOW
270
+
271
+ For complex tasks, make multiple tool calls:
272
+ 1. First, list or search to find relevant records
273
+ 2. Then, get details or create/update as needed
274
+ 3. Finally, call submit_answer with your findings
237
275
 
238
- AVAILABLE OPERATIONS:
239
- - Companies: list, get, create, update, delete
240
- - People/Contacts: list, get, create, update, delete
241
- - Opportunities: list, get, create, update, delete
242
- - Notes: list, create (attach to companies, people, or opportunities)
243
- - Tasks: list, create, update, complete
276
+ ## OUTPUT FORMAT
244
277
 
245
- EXAMPLES:
278
+ Think briefly about which tool to use, then output exactly one tool call:
279
+ <tool_call>
280
+ {"name": "tool_name", "arguments": {...}}
281
+ </tool_call>
246
282
 
247
- 1. List companies:
283
+ ## EXAMPLES
284
+
285
+ List companies:
248
286
  <tool_call>
249
287
  {"name": "list_companies", "arguments": {"limit": 10}}
250
288
  </tool_call>
251
289
 
252
- 2. Create a company:
290
+ Find companies by name:
253
291
  <tool_call>
254
- {"name": "create_company", "arguments": {"company_name": "Acme Corp", "company_domain": "acme.com"}}
292
+ {"name": "list_companies", "arguments": {"filter": "name[ilike]:\"%tech%\""}}
255
293
  </tool_call>
256
294
 
257
- 3. Create a contact:
295
+ Create a company:
258
296
  <tool_call>
259
- {"name": "create_person", "arguments": {"person_first_name": "John", "person_last_name": "Doe", "person_email": "john@acme.com"}}
297
+ {"name": "create_company", "arguments": {"company_name": "Acme Corp", "company_domain": "acme.com"}}
260
298
  </tool_call>
261
299
 
262
- 4. Submit final answer:
300
+ Create a contact linked to a company:
263
301
  <tool_call>
264
- {"name": "submit_answer", "arguments": {"answer": "The total pipeline value is $1.5M"}}
302
+ {"name": "create_person", "arguments": {"person_first_name": "John", "person_last_name": "Doe", "person_email": "john@acme.com", "person_company_id": "company-uuid-here"}}
265
303
  </tool_call>
266
304
 
267
- IMPORTANT: Output ONLY a tool_call, no other text."""
305
+ Submit final answer:
306
+ <tool_call>
307
+ {"name": "submit_answer", "arguments": {"answer": "The total pipeline value is $1.5M across 12 open opportunities."}}
308
+ </tool_call>"""
268
309
 
269
310
  @property
270
311
  def tools(self) -> list[JsonDict]:
271
312
  """Tool definitions for the CRM environment.
272
313
 
273
- Override this property in a subclass to customize available tools.
274
- You can filter, extend, or replace the default tool set.
314
+ Returns tool definitions formatted by `format_tools()`. Override
315
+ `format_tools()` to transform the tool schema for different providers
316
+ (e.g., Anthropic, Google).
275
317
 
276
318
  Returns:
277
- List of tool definitions in OpenAI function calling format.
319
+ List of tool definitions (OpenAI format by default).
278
320
 
279
321
  Example:
280
322
  >>> class ReadOnlyAgent(CrmAgentEnv):
@@ -285,7 +327,34 @@ IMPORTANT: Output ONLY a tool_call, no other text."""
285
327
  ... return [t for t in self._default_tools()
286
328
  ... if any(op in t['function']['name'] for op in read_ops)]
287
329
  """
288
- return self._default_tools()
330
+ return self.format_tools(self._default_tools())
331
+
332
+ def format_tools(self, tools: list[JsonDict]) -> list[JsonDict]:
333
+ """Format tool definitions for the target LLM provider.
334
+
335
+ Override this method to transform tool schemas for different providers.
336
+ The default implementation returns OpenAI-compatible format unchanged.
337
+
338
+ Args:
339
+ tools: List of tool definitions in OpenAI format.
340
+
341
+ Returns:
342
+ Formatted tool definitions for your target provider.
343
+
344
+ Example (Anthropic format):
345
+ >>> class AnthropicCrmAgent(CrmAgentEnv):
346
+ ... def format_tools(self, tools):
347
+ ... # Convert OpenAI format to Anthropic format
348
+ ... return [
349
+ ... {
350
+ ... "name": t["function"]["name"],
351
+ ... "description": t["function"]["description"],
352
+ ... "input_schema": t["function"]["parameters"],
353
+ ... }
354
+ ... for t in tools
355
+ ... ]
356
+ """
357
+ return tools
289
358
 
290
359
  def _default_tools(self) -> list[JsonDict]:
291
360
  """Return the default tool definitions.
@@ -293,548 +362,9 @@ IMPORTANT: Output ONLY a tool_call, no other text."""
293
362
  Subclasses can call this to get all default tools and filter/extend them.
294
363
 
295
364
  Returns:
296
- Complete list of CRM tool definitions.
365
+ Complete list of CRM tool definitions in OpenAI format.
297
366
  """
298
- return [
299
- # =================================================================
300
- # Company Tools
301
- # =================================================================
302
- {
303
- "type": "function",
304
- "function": {
305
- "name": "list_companies",
306
- "description": "List all companies in the CRM",
307
- "parameters": {
308
- "type": "object",
309
- "properties": {
310
- "limit": {
311
- "type": "integer",
312
- "default": 60,
313
- "description": "Maximum number of companies to return (max 200)",
314
- },
315
- "starting_after": {
316
- "type": "string",
317
- "description": "Cursor for pagination - returns objects after this ID",
318
- },
319
- "ending_before": {
320
- "type": "string",
321
- "description": "Cursor for pagination - returns objects before this ID",
322
- },
323
- "order_by": {
324
- "type": "string",
325
- "description": "Order by: field_name[ASC|DESC]",
326
- },
327
- "filter": {
328
- "type": "string",
329
- "description": "Filter: field[eq|gt|lt|contains]:value",
330
- },
331
- "depth": {
332
- "type": "integer",
333
- "default": 1,
334
- "description": "Relation depth: 0=primary only, 1=include relations",
335
- },
336
- },
337
- "required": [],
338
- },
339
- },
340
- },
341
- {
342
- "type": "function",
343
- "function": {
344
- "name": "get_company",
345
- "description": "Get details of a specific company",
346
- "parameters": {
347
- "type": "object",
348
- "properties": {
349
- "record_id": {
350
- "type": "string",
351
- "description": "ID of the company to retrieve",
352
- },
353
- "depth": {
354
- "type": "integer",
355
- "default": 1,
356
- "description": "Relation depth: 0=primary only, 1=include relations",
357
- },
358
- },
359
- "required": ["record_id"],
360
- },
361
- },
362
- },
363
- {
364
- "type": "function",
365
- "function": {
366
- "name": "create_company",
367
- "description": "Create a new company in the CRM",
368
- "parameters": {
369
- "type": "object",
370
- "properties": {
371
- "company_name": {
372
- "type": "string",
373
- "description": "Name of the company",
374
- },
375
- "company_domain": {
376
- "type": "string",
377
- "description": "Domain/website of the company",
378
- },
379
- "company_address": {
380
- "type": "string",
381
- "description": "Address of the company",
382
- },
383
- "company_employees": {
384
- "type": "integer",
385
- "description": "Number of employees",
386
- },
387
- },
388
- "required": ["company_name"],
389
- },
390
- },
391
- },
392
- {
393
- "type": "function",
394
- "function": {
395
- "name": "update_company",
396
- "description": "Update an existing company",
397
- "parameters": {
398
- "type": "object",
399
- "properties": {
400
- "record_id": {
401
- "type": "string",
402
- "description": "ID of the company to update",
403
- },
404
- "company_name": {"type": "string"},
405
- "company_domain": {"type": "string"},
406
- "company_address": {"type": "string"},
407
- "company_employees": {"type": "integer"},
408
- },
409
- "required": ["record_id"],
410
- },
411
- },
412
- },
413
- {
414
- "type": "function",
415
- "function": {
416
- "name": "delete_company",
417
- "description": "Delete a company from the CRM",
418
- "parameters": {
419
- "type": "object",
420
- "properties": {
421
- "record_id": {
422
- "type": "string",
423
- "description": "ID of the company to delete",
424
- },
425
- },
426
- "required": ["record_id"],
427
- },
428
- },
429
- },
430
- # =================================================================
431
- # Person/Contact Tools
432
- # =================================================================
433
- {
434
- "type": "function",
435
- "function": {
436
- "name": "list_people",
437
- "description": "List all contacts/people in the CRM",
438
- "parameters": {
439
- "type": "object",
440
- "properties": {
441
- "limit": {
442
- "type": "integer",
443
- "default": 60,
444
- "description": "Maximum number of contacts to return (max 200)",
445
- },
446
- "starting_after": {"type": "string"},
447
- "ending_before": {"type": "string"},
448
- "order_by": {"type": "string"},
449
- "filter": {"type": "string"},
450
- "depth": {"type": "integer", "default": 1},
451
- },
452
- "required": [],
453
- },
454
- },
455
- },
456
- {
457
- "type": "function",
458
- "function": {
459
- "name": "get_person",
460
- "description": "Get details of a specific contact",
461
- "parameters": {
462
- "type": "object",
463
- "properties": {
464
- "record_id": {
465
- "type": "string",
466
- "description": "ID of the contact to retrieve",
467
- },
468
- "depth": {"type": "integer", "default": 1},
469
- },
470
- "required": ["record_id"],
471
- },
472
- },
473
- },
474
- {
475
- "type": "function",
476
- "function": {
477
- "name": "create_person",
478
- "description": "Create a new contact/person in the CRM",
479
- "parameters": {
480
- "type": "object",
481
- "properties": {
482
- "person_first_name": {
483
- "type": "string",
484
- "description": "First name",
485
- },
486
- "person_last_name": {
487
- "type": "string",
488
- "description": "Last name",
489
- },
490
- "person_email": {
491
- "type": "string",
492
- "description": "Email address",
493
- },
494
- "person_phone": {
495
- "type": "string",
496
- "description": "Phone number",
497
- },
498
- "person_company_id": {
499
- "type": "string",
500
- "description": "ID of associated company",
501
- },
502
- "person_job_title": {
503
- "type": "string",
504
- "description": "Job title",
505
- },
506
- },
507
- "required": ["person_first_name", "person_last_name"],
508
- },
509
- },
510
- },
511
- {
512
- "type": "function",
513
- "function": {
514
- "name": "update_person",
515
- "description": "Update an existing contact",
516
- "parameters": {
517
- "type": "object",
518
- "properties": {
519
- "record_id": {
520
- "type": "string",
521
- "description": "ID of the contact to update",
522
- },
523
- "person_first_name": {"type": "string"},
524
- "person_last_name": {"type": "string"},
525
- "person_email": {"type": "string"},
526
- "person_phone": {"type": "string"},
527
- "person_job_title": {"type": "string"},
528
- },
529
- "required": ["record_id"],
530
- },
531
- },
532
- },
533
- {
534
- "type": "function",
535
- "function": {
536
- "name": "delete_person",
537
- "description": "Delete a contact from the CRM",
538
- "parameters": {
539
- "type": "object",
540
- "properties": {
541
- "record_id": {
542
- "type": "string",
543
- "description": "ID of the contact to delete",
544
- },
545
- },
546
- "required": ["record_id"],
547
- },
548
- },
549
- },
550
- # =================================================================
551
- # Opportunity Tools
552
- # =================================================================
553
- {
554
- "type": "function",
555
- "function": {
556
- "name": "list_opportunities",
557
- "description": "List all opportunities/deals in the CRM",
558
- "parameters": {
559
- "type": "object",
560
- "properties": {
561
- "limit": {
562
- "type": "integer",
563
- "default": 60,
564
- "description": "Maximum number to return (max 200)",
565
- },
566
- "starting_after": {"type": "string"},
567
- "ending_before": {"type": "string"},
568
- "order_by": {"type": "string"},
569
- "filter": {"type": "string"},
570
- "depth": {"type": "integer", "default": 1},
571
- },
572
- "required": [],
573
- },
574
- },
575
- },
576
- {
577
- "type": "function",
578
- "function": {
579
- "name": "get_opportunity",
580
- "description": "Get details of a specific opportunity",
581
- "parameters": {
582
- "type": "object",
583
- "properties": {
584
- "record_id": {
585
- "type": "string",
586
- "description": "ID of the opportunity",
587
- },
588
- "depth": {"type": "integer", "default": 1},
589
- },
590
- "required": ["record_id"],
591
- },
592
- },
593
- },
594
- {
595
- "type": "function",
596
- "function": {
597
- "name": "create_opportunity",
598
- "description": "Create a new opportunity/deal",
599
- "parameters": {
600
- "type": "object",
601
- "properties": {
602
- "opportunity_name": {
603
- "type": "string",
604
- "description": "Name of the opportunity",
605
- },
606
- "opportunity_amount": {
607
- "type": "number",
608
- "description": "Deal value",
609
- },
610
- "opportunity_stage": {
611
- "type": "string",
612
- "enum": ["NEW", "MEETING", "PROPOSAL", "WON", "LOST"],
613
- "description": "Sales stage",
614
- },
615
- "opportunity_close_date": {
616
- "type": "string",
617
- "description": "Expected close date (ISO format)",
618
- },
619
- "opportunity_company_id": {
620
- "type": "string",
621
- "description": "Associated company ID",
622
- },
623
- "opportunity_person_id": {
624
- "type": "string",
625
- "description": "Point of contact ID",
626
- },
627
- },
628
- "required": ["opportunity_name"],
629
- },
630
- },
631
- },
632
- {
633
- "type": "function",
634
- "function": {
635
- "name": "update_opportunity",
636
- "description": "Update an existing opportunity",
637
- "parameters": {
638
- "type": "object",
639
- "properties": {
640
- "record_id": {
641
- "type": "string",
642
- "description": "ID of the opportunity to update",
643
- },
644
- "opportunity_name": {"type": "string"},
645
- "opportunity_amount": {"type": "number"},
646
- "opportunity_stage": {
647
- "type": "string",
648
- "enum": ["NEW", "MEETING", "PROPOSAL", "WON", "LOST"],
649
- },
650
- "opportunity_close_date": {"type": "string"},
651
- },
652
- "required": ["record_id"],
653
- },
654
- },
655
- },
656
- {
657
- "type": "function",
658
- "function": {
659
- "name": "delete_opportunity",
660
- "description": "Delete an opportunity",
661
- "parameters": {
662
- "type": "object",
663
- "properties": {
664
- "record_id": {
665
- "type": "string",
666
- "description": "ID of the opportunity to delete",
667
- },
668
- },
669
- "required": ["record_id"],
670
- },
671
- },
672
- },
673
- # =================================================================
674
- # Note Tools
675
- # =================================================================
676
- {
677
- "type": "function",
678
- "function": {
679
- "name": "list_notes",
680
- "description": "List all notes in the CRM",
681
- "parameters": {
682
- "type": "object",
683
- "properties": {
684
- "limit": {
685
- "type": "integer",
686
- "default": 10,
687
- "description": "Maximum number of notes to return",
688
- },
689
- },
690
- "required": [],
691
- },
692
- },
693
- },
694
- {
695
- "type": "function",
696
- "function": {
697
- "name": "create_note",
698
- "description": "Create a note attached to a record",
699
- "parameters": {
700
- "type": "object",
701
- "properties": {
702
- "note_body": {
703
- "type": "string",
704
- "description": "Content of the note",
705
- },
706
- "note_target_id": {
707
- "type": "string",
708
- "description": "ID of record to attach note to",
709
- },
710
- "note_target_type": {
711
- "type": "string",
712
- "enum": ["company", "person", "opportunity"],
713
- "description": "Type of record",
714
- },
715
- },
716
- "required": ["note_body"],
717
- },
718
- },
719
- },
720
- # =================================================================
721
- # Task Tools
722
- # =================================================================
723
- {
724
- "type": "function",
725
- "function": {
726
- "name": "list_tasks",
727
- "description": "List all tasks in the CRM",
728
- "parameters": {
729
- "type": "object",
730
- "properties": {
731
- "limit": {
732
- "type": "integer",
733
- "default": 10,
734
- "description": "Maximum number of tasks to return",
735
- },
736
- },
737
- "required": [],
738
- },
739
- },
740
- },
741
- {
742
- "type": "function",
743
- "function": {
744
- "name": "create_task",
745
- "description": "Create a task, optionally linked to a record",
746
- "parameters": {
747
- "type": "object",
748
- "properties": {
749
- "task_title": {
750
- "type": "string",
751
- "description": "Title of the task",
752
- },
753
- "task_body": {
754
- "type": "string",
755
- "description": "Description",
756
- },
757
- "task_due_date": {
758
- "type": "string",
759
- "description": "Due date (ISO format)",
760
- },
761
- "task_status": {
762
- "type": "string",
763
- "enum": ["TODO", "IN_PROGRESS", "DONE"],
764
- "description": "Status",
765
- },
766
- "task_target_id": {
767
- "type": "string",
768
- "description": "ID of record to link task to",
769
- },
770
- "task_target_type": {
771
- "type": "string",
772
- "enum": ["company", "person", "opportunity"],
773
- "description": "Type of record",
774
- },
775
- },
776
- "required": ["task_title"],
777
- },
778
- },
779
- },
780
- {
781
- "type": "function",
782
- "function": {
783
- "name": "update_task",
784
- "description": "Update an existing task",
785
- "parameters": {
786
- "type": "object",
787
- "properties": {
788
- "record_id": {
789
- "type": "string",
790
- "description": "ID of the task to update",
791
- },
792
- "task_title": {"type": "string"},
793
- "task_body": {"type": "string"},
794
- "task_due_date": {"type": "string"},
795
- },
796
- "required": ["record_id"],
797
- },
798
- },
799
- },
800
- {
801
- "type": "function",
802
- "function": {
803
- "name": "complete_task",
804
- "description": "Mark a task as complete",
805
- "parameters": {
806
- "type": "object",
807
- "properties": {
808
- "record_id": {
809
- "type": "string",
810
- "description": "ID of the task to complete",
811
- },
812
- },
813
- "required": ["record_id"],
814
- },
815
- },
816
- },
817
- # =================================================================
818
- # Submit Answer Tool
819
- # =================================================================
820
- {
821
- "type": "function",
822
- "function": {
823
- "name": "submit_answer",
824
- "description": "Submit final answer and end the session",
825
- "parameters": {
826
- "type": "object",
827
- "properties": {
828
- "answer": {
829
- "type": "string",
830
- "description": "The final answer based on CRM data",
831
- },
832
- },
833
- "required": ["answer"],
834
- },
835
- },
836
- },
837
- ]
367
+ return list(DEFAULT_TOOLS)
838
368
 
839
369
  # =========================================================================
840
370
  # Tool Parsing and Observation Formatting
@@ -967,6 +497,32 @@ IMPORTANT: Output ONLY a tool_call, no other text."""
967
497
  if field in arguments and arguments[field] is not None:
968
498
  action_kwargs[field] = arguments[field]
969
499
 
500
+ # Convert generic note_target_id/type to specific fields
501
+ # Tool schema uses: note_target_id + note_target_type
502
+ # Server expects: note_target_person_id, note_target_company_id, etc.
503
+ note_target_id = action_kwargs.pop("note_target_id", None)
504
+ note_target_type = action_kwargs.pop("note_target_type", None)
505
+ if note_target_id and note_target_type:
506
+ target_type = str(note_target_type).lower()
507
+ if target_type == "person":
508
+ action_kwargs["note_target_person_id"] = note_target_id
509
+ elif target_type == "company":
510
+ action_kwargs["note_target_company_id"] = note_target_id
511
+ elif target_type == "opportunity":
512
+ action_kwargs["note_target_opportunity_id"] = note_target_id
513
+
514
+ # Same conversion for tasks
515
+ task_target_id = action_kwargs.pop("task_target_id", None)
516
+ task_target_type = action_kwargs.pop("task_target_type", None)
517
+ if task_target_id and task_target_type:
518
+ target_type = str(task_target_type).lower()
519
+ if target_type == "person":
520
+ action_kwargs["task_target_person_id"] = task_target_id
521
+ elif target_type == "company":
522
+ action_kwargs["task_target_company_id"] = task_target_id
523
+ elif target_type == "opportunity":
524
+ action_kwargs["task_target_opportunity_id"] = task_target_id
525
+
970
526
  try:
971
527
  action = CrmAgentAction(**action_kwargs)
972
528
  return ParsedAction(action=action, is_valid=True)