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