construct-labs-crm-env 0.1.1__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.
- construct_labs_crm_env/__init__.py +33 -0
- construct_labs_crm_env/_vendored/LICENSE.openenv +28 -0
- construct_labs_crm_env/_vendored/__init__.py +14 -0
- construct_labs_crm_env/_vendored/_client.py +216 -0
- construct_labs_crm_env/_vendored/_types.py +82 -0
- construct_labs_crm_env/client.py +1004 -0
- construct_labs_crm_env/models.py +260 -0
- construct_labs_crm_env/protocol.py +25 -0
- construct_labs_crm_env/py.typed +0 -0
- construct_labs_crm_env-0.1.1.dist-info/METADATA +412 -0
- construct_labs_crm_env-0.1.1.dist-info/RECORD +13 -0
- construct_labs_crm_env-0.1.1.dist-info/WHEEL +4 -0
- construct_labs_crm_env-0.1.1.dist-info/licenses/LICENSE +42 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Data models for the CRM Agent Environment.
|
|
2
|
+
|
|
3
|
+
This module provides the core data types for interacting with the CRM Agent
|
|
4
|
+
Environment, including actions, observations, and state representations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import ConfigDict, Field
|
|
13
|
+
|
|
14
|
+
from ._vendored import Action, Observation, State
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CRMActionType(str, Enum):
|
|
18
|
+
"""Types of actions available in the CRM environment.
|
|
19
|
+
|
|
20
|
+
Actions are grouped by the CRM entity they operate on:
|
|
21
|
+
- Company: CRUD operations for company records
|
|
22
|
+
- Person: CRUD operations for contact/person records
|
|
23
|
+
- Opportunity: CRUD operations for sales opportunities
|
|
24
|
+
- Note: Create and list notes attached to records
|
|
25
|
+
- Task: CRUD operations for tasks
|
|
26
|
+
- Answer: Submit final answer for evaluation
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# Company actions
|
|
30
|
+
CREATE_COMPANY = "create_company"
|
|
31
|
+
UPDATE_COMPANY = "update_company"
|
|
32
|
+
DELETE_COMPANY = "delete_company"
|
|
33
|
+
LIST_COMPANIES = "list_companies"
|
|
34
|
+
GET_COMPANY = "get_company"
|
|
35
|
+
|
|
36
|
+
# Person/Contact actions
|
|
37
|
+
CREATE_PERSON = "create_person"
|
|
38
|
+
UPDATE_PERSON = "update_person"
|
|
39
|
+
DELETE_PERSON = "delete_person"
|
|
40
|
+
LIST_PEOPLE = "list_people"
|
|
41
|
+
GET_PERSON = "get_person"
|
|
42
|
+
|
|
43
|
+
# Opportunity actions
|
|
44
|
+
CREATE_OPPORTUNITY = "create_opportunity"
|
|
45
|
+
UPDATE_OPPORTUNITY = "update_opportunity"
|
|
46
|
+
DELETE_OPPORTUNITY = "delete_opportunity"
|
|
47
|
+
LIST_OPPORTUNITIES = "list_opportunities"
|
|
48
|
+
GET_OPPORTUNITY = "get_opportunity"
|
|
49
|
+
|
|
50
|
+
# Note actions
|
|
51
|
+
CREATE_NOTE = "create_note"
|
|
52
|
+
LIST_NOTES = "list_notes"
|
|
53
|
+
|
|
54
|
+
# Task actions
|
|
55
|
+
CREATE_TASK = "create_task"
|
|
56
|
+
UPDATE_TASK = "update_task"
|
|
57
|
+
COMPLETE_TASK = "complete_task"
|
|
58
|
+
LIST_TASKS = "list_tasks"
|
|
59
|
+
|
|
60
|
+
# Final answer
|
|
61
|
+
SUBMIT_ANSWER = "submit_answer"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CrmAgentAction(Action):
|
|
65
|
+
"""Action to execute in the CRM environment.
|
|
66
|
+
|
|
67
|
+
This model represents all possible actions an agent can take. The `action_type`
|
|
68
|
+
field determines which operation to perform, and the relevant fields for that
|
|
69
|
+
action type should be populated.
|
|
70
|
+
|
|
71
|
+
Example:
|
|
72
|
+
>>> # List companies
|
|
73
|
+
>>> action = CrmAgentAction(
|
|
74
|
+
... action_type=CRMActionType.LIST_COMPANIES,
|
|
75
|
+
... limit=10
|
|
76
|
+
... )
|
|
77
|
+
|
|
78
|
+
>>> # Create a company
|
|
79
|
+
>>> action = CrmAgentAction(
|
|
80
|
+
... action_type=CRMActionType.CREATE_COMPANY,
|
|
81
|
+
... company_name="Acme Corp",
|
|
82
|
+
... company_domain="acme.com"
|
|
83
|
+
... )
|
|
84
|
+
|
|
85
|
+
>>> # Submit final answer
|
|
86
|
+
>>> action = CrmAgentAction(
|
|
87
|
+
... action_type=CRMActionType.SUBMIT_ANSWER,
|
|
88
|
+
... answer="The total revenue is $1.5M"
|
|
89
|
+
... )
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
model_config = ConfigDict(extra="allow")
|
|
93
|
+
|
|
94
|
+
action_type: CRMActionType = Field(..., description="Type of CRM action to perform")
|
|
95
|
+
|
|
96
|
+
# Common fields
|
|
97
|
+
record_id: str | None = Field(
|
|
98
|
+
default=None,
|
|
99
|
+
description="ID of the record to operate on (for get/update/delete)",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Company fields
|
|
103
|
+
company_name: str | None = Field(default=None, description="Name of the company")
|
|
104
|
+
company_domain: str | None = Field(
|
|
105
|
+
default=None, description="Domain/website of the company"
|
|
106
|
+
)
|
|
107
|
+
company_address: str | None = Field(
|
|
108
|
+
default=None, description="Address of the company"
|
|
109
|
+
)
|
|
110
|
+
company_employees: int | None = Field(
|
|
111
|
+
default=None, description="Number of employees"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Person fields
|
|
115
|
+
person_first_name: str | None = Field(
|
|
116
|
+
default=None, description="First name of the person"
|
|
117
|
+
)
|
|
118
|
+
person_last_name: str | None = Field(
|
|
119
|
+
default=None, description="Last name of the person"
|
|
120
|
+
)
|
|
121
|
+
person_email: str | None = Field(default=None, description="Email of the person")
|
|
122
|
+
person_phone: str | None = Field(
|
|
123
|
+
default=None, description="Phone number of the person"
|
|
124
|
+
)
|
|
125
|
+
person_company_id: str | None = Field(
|
|
126
|
+
default=None, description="ID of the company this person belongs to"
|
|
127
|
+
)
|
|
128
|
+
person_job_title: str | None = Field(
|
|
129
|
+
default=None, description="Job title of the person"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Opportunity fields
|
|
133
|
+
opportunity_name: str | None = Field(
|
|
134
|
+
default=None, description="Name of the opportunity"
|
|
135
|
+
)
|
|
136
|
+
opportunity_amount: float | None = Field(default=None, description="Deal amount")
|
|
137
|
+
opportunity_stage: str | None = Field(
|
|
138
|
+
default=None,
|
|
139
|
+
description="Stage: 'NEW', 'MEETING', 'PROPOSAL', 'WON', or 'LOST'",
|
|
140
|
+
)
|
|
141
|
+
opportunity_close_date: str | None = Field(
|
|
142
|
+
default=None, description="Expected close date (ISO format)"
|
|
143
|
+
)
|
|
144
|
+
opportunity_company_id: str | None = Field(
|
|
145
|
+
default=None, description="ID of the company for this opportunity"
|
|
146
|
+
)
|
|
147
|
+
opportunity_person_id: str | None = Field(
|
|
148
|
+
default=None, description="ID of the contact person for this opportunity"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Note fields
|
|
152
|
+
note_body: str | None = Field(default=None, description="Content of the note")
|
|
153
|
+
note_title: str | None = Field(default=None, description="Title of the note")
|
|
154
|
+
note_target_person_id: str | None = Field(
|
|
155
|
+
default=None, description="ID of the person to link this note to"
|
|
156
|
+
)
|
|
157
|
+
note_target_company_id: str | None = Field(
|
|
158
|
+
default=None, description="ID of the company to link this note to"
|
|
159
|
+
)
|
|
160
|
+
note_target_opportunity_id: str | None = Field(
|
|
161
|
+
default=None, description="ID of the opportunity to link this note to"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Task fields
|
|
165
|
+
task_title: str | None = Field(default=None, description="Title of the task")
|
|
166
|
+
task_body: str | None = Field(default=None, description="Description of the task")
|
|
167
|
+
task_due_date: str | None = Field(default=None, description="Due date (ISO format)")
|
|
168
|
+
task_assignee_id: str | None = Field(
|
|
169
|
+
default=None, description="ID of the user to assign the task to"
|
|
170
|
+
)
|
|
171
|
+
task_status: str | None = Field(default=None, description="Status of the task")
|
|
172
|
+
task_target_person_id: str | None = Field(
|
|
173
|
+
default=None, description="ID of the person to link this task to"
|
|
174
|
+
)
|
|
175
|
+
task_target_company_id: str | None = Field(
|
|
176
|
+
default=None, description="ID of the company to link this task to"
|
|
177
|
+
)
|
|
178
|
+
task_target_opportunity_id: str | None = Field(
|
|
179
|
+
default=None, description="ID of the opportunity to link this task to"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Final answer
|
|
183
|
+
answer: str | None = Field(default=None, description="The final answer to submit")
|
|
184
|
+
|
|
185
|
+
# Pagination and filtering
|
|
186
|
+
limit: int | None = Field(
|
|
187
|
+
default=60, description="Maximum number of results to return (max 200)"
|
|
188
|
+
)
|
|
189
|
+
cursor: str | None = Field(
|
|
190
|
+
default=None, description="Pagination cursor (legacy, use starting_after)"
|
|
191
|
+
)
|
|
192
|
+
starting_after: str | None = Field(
|
|
193
|
+
default=None, description="Returns objects starting after this cursor"
|
|
194
|
+
)
|
|
195
|
+
ending_before: str | None = Field(
|
|
196
|
+
default=None, description="Returns objects ending before this cursor"
|
|
197
|
+
)
|
|
198
|
+
order_by: str | None = Field(
|
|
199
|
+
default=None, description="Order by: field_name_1,field_name_2[DIRECTION]"
|
|
200
|
+
)
|
|
201
|
+
filter: str | None = Field(
|
|
202
|
+
default=None, description="Filter: field[COMPARATOR]:value"
|
|
203
|
+
)
|
|
204
|
+
depth: int | None = Field(
|
|
205
|
+
default=1, description="Nesting depth: 0=primary, 1=direct relations"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class CrmAgentObservation(Observation):
|
|
210
|
+
"""Observation returned after executing an action.
|
|
211
|
+
|
|
212
|
+
Contains the result of the action, including any data returned by the CRM
|
|
213
|
+
and error information if the action failed.
|
|
214
|
+
|
|
215
|
+
Attributes:
|
|
216
|
+
success: Whether the action completed successfully.
|
|
217
|
+
error: Error message if the action failed, None otherwise.
|
|
218
|
+
data: Raw response data from the CRM server.
|
|
219
|
+
done: Whether the episode has ended (inherited from Observation).
|
|
220
|
+
reward: Reward signal if applicable (inherited from Observation).
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
success: bool = Field(default=True, description="Whether the action succeeded")
|
|
224
|
+
error: str | None = Field(
|
|
225
|
+
default=None, description="Error message if action failed"
|
|
226
|
+
)
|
|
227
|
+
data: dict[str, Any] = Field(
|
|
228
|
+
default_factory=dict, description="Raw response data from server"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class CrmAgentState(State):
|
|
233
|
+
"""Internal state of the CRM environment session.
|
|
234
|
+
|
|
235
|
+
Tracks metadata about the current episode including entity counts
|
|
236
|
+
and session information.
|
|
237
|
+
|
|
238
|
+
Attributes:
|
|
239
|
+
env_name: Name of the environment.
|
|
240
|
+
done: Whether the episode has ended.
|
|
241
|
+
terminated: Whether the episode terminated normally.
|
|
242
|
+
truncated: Whether the episode was truncated (e.g., step limit).
|
|
243
|
+
success: Whether the task was completed successfully.
|
|
244
|
+
companies_count: Number of companies in the CRM.
|
|
245
|
+
people_count: Number of people/contacts in the CRM.
|
|
246
|
+
opportunities_count: Number of opportunities in the CRM.
|
|
247
|
+
last_action: String representation of the last action taken.
|
|
248
|
+
api_url: The CRM API endpoint URL.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
env_name: str = "crm-agent"
|
|
252
|
+
done: bool = False
|
|
253
|
+
terminated: bool = False
|
|
254
|
+
truncated: bool = False
|
|
255
|
+
success: bool = False
|
|
256
|
+
companies_count: int = 0
|
|
257
|
+
people_count: int = 0
|
|
258
|
+
opportunities_count: int = 0
|
|
259
|
+
last_action: str | None = None
|
|
260
|
+
api_url: str = ""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Protocol types for CRM Agent Environment.
|
|
2
|
+
|
|
3
|
+
This module provides the ParsedAction dataclass used for parsing
|
|
4
|
+
tool calls from LLM outputs into environment actions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ParsedAction:
|
|
15
|
+
"""Result of parsing a tool call into an environment action.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
action: The parsed action object, or None if parsing failed.
|
|
19
|
+
is_valid: Whether the tool call was successfully parsed.
|
|
20
|
+
error_message: Human-readable error message if parsing failed.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
action: Any | None
|
|
24
|
+
is_valid: bool
|
|
25
|
+
error_message: str | None = None
|
|
File without changes
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: construct-labs-crm-env
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: CRM Agent Environment SDK by Construct Labs - Train RL agents to interact with CRM systems
|
|
5
|
+
Project-URL: Homepage, https://construct-labs.com
|
|
6
|
+
Author-email: Construct Labs GmbH <hello@construct-labs.com>
|
|
7
|
+
License: Proprietary
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: agent,ai,crm,environment,machine-learning,reinforcement-learning
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: Other/Proprietary License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: pydantic>=2.0.0
|
|
23
|
+
Requires-Dist: websockets>=12.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Construct Labs CRM Agent Environment
|
|
32
|
+
|
|
33
|
+
Python SDK for the Construct Labs CRM Agent Environment - a reinforcement learning environment for training AI agents to interact with CRM systems.
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
This software requires a commercial license from Construct Labs GmbH.
|
|
38
|
+
Contact hello@construct-labs.com for licensing inquiries.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install construct-labs-crm-env
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from construct_labs_crm_env import CrmAgentEnv, CrmAgentAction, CRMActionType
|
|
50
|
+
|
|
51
|
+
# Connect to the CRM environment
|
|
52
|
+
with CrmAgentEnv(
|
|
53
|
+
base_url="https://api.construct-labs.com",
|
|
54
|
+
api_key="your-api-key" # Issued by Construct Labs
|
|
55
|
+
) as env:
|
|
56
|
+
# Reset the environment
|
|
57
|
+
result = env.reset()
|
|
58
|
+
|
|
59
|
+
# List companies
|
|
60
|
+
result = env.step(CrmAgentAction(
|
|
61
|
+
action_type=CRMActionType.LIST_COMPANIES,
|
|
62
|
+
limit=10
|
|
63
|
+
))
|
|
64
|
+
print(result.observation.data)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Environment Variables
|
|
68
|
+
|
|
69
|
+
You can set your API key via environment variable:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
export CRM_AGENT_API_KEY=your-api-key
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# API key is read from environment
|
|
77
|
+
env = CrmAgentEnv(base_url="https://api.construct-labs.com")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## LLM Integration Example
|
|
81
|
+
|
|
82
|
+
The SDK is designed to work with LLM-based agents. Here's how to parse LLM tool calls:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from construct_labs_crm_env import CrmAgentEnv
|
|
86
|
+
|
|
87
|
+
with CrmAgentEnv(
|
|
88
|
+
base_url="https://api.construct-labs.com",
|
|
89
|
+
api_key="your-api-key"
|
|
90
|
+
) as env:
|
|
91
|
+
result = env.reset()
|
|
92
|
+
|
|
93
|
+
# Simulate an LLM generating a tool call
|
|
94
|
+
llm_tool_call = {
|
|
95
|
+
"name": "list_companies",
|
|
96
|
+
"arguments": {"limit": 5}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Parse the tool call into a CrmAgentAction
|
|
100
|
+
parsed = env.parse_tool_call(llm_tool_call)
|
|
101
|
+
|
|
102
|
+
if parsed.is_valid:
|
|
103
|
+
result = env.step(parsed.action)
|
|
104
|
+
print(result.observation.model_dump_json(indent=2))
|
|
105
|
+
else:
|
|
106
|
+
print(f"Invalid tool call: {parsed.error_message}")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Customization
|
|
110
|
+
|
|
111
|
+
Subclass `CrmAgentEnv` to customize agent behavior:
|
|
112
|
+
|
|
113
|
+
### Custom System Prompt
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
class SalesAgent(CrmAgentEnv):
|
|
117
|
+
@property
|
|
118
|
+
def system_prompt(self) -> str:
|
|
119
|
+
return """You are a sales assistant AI.
|
|
120
|
+
|
|
121
|
+
Your goal is to help close deals by:
|
|
122
|
+
1. Finding relevant companies and contacts
|
|
123
|
+
2. Creating opportunities with accurate values
|
|
124
|
+
3. Adding follow-up tasks
|
|
125
|
+
|
|
126
|
+
Be concise. Focus on high-value opportunities."""
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Restricted Tool Set
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
class ReadOnlyAgent(CrmAgentEnv):
|
|
133
|
+
"""Agent that can only read data, not modify."""
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def tools(self) -> list[dict]:
|
|
137
|
+
read_only = {'list_companies', 'get_company', 'list_people',
|
|
138
|
+
'get_person', 'list_opportunities', 'submit_answer'}
|
|
139
|
+
return [t for t in self._default_tools()
|
|
140
|
+
if t['function']['name'] in read_only]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Custom Observation Formatting
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
class VerboseAgent(CrmAgentEnv):
|
|
147
|
+
def format_observation(self, observation):
|
|
148
|
+
base = super().format_observation(observation)
|
|
149
|
+
return f"=== CRM Response ===\n{base}\n=== End ==="
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Available Actions
|
|
153
|
+
|
|
154
|
+
### Company Operations
|
|
155
|
+
- `LIST_COMPANIES` - List all companies
|
|
156
|
+
- `GET_COMPANY` - Get a specific company by ID
|
|
157
|
+
- `CREATE_COMPANY` - Create a new company
|
|
158
|
+
- `UPDATE_COMPANY` - Update an existing company
|
|
159
|
+
- `DELETE_COMPANY` - Delete a company
|
|
160
|
+
|
|
161
|
+
### Contact Operations
|
|
162
|
+
- `LIST_PEOPLE` - List all contacts
|
|
163
|
+
- `GET_PERSON` - Get a specific contact by ID
|
|
164
|
+
- `CREATE_PERSON` - Create a new contact
|
|
165
|
+
- `UPDATE_PERSON` - Update an existing contact
|
|
166
|
+
- `DELETE_PERSON` - Delete a contact
|
|
167
|
+
|
|
168
|
+
### Opportunity Operations
|
|
169
|
+
- `LIST_OPPORTUNITIES` - List all opportunities
|
|
170
|
+
- `GET_OPPORTUNITY` - Get a specific opportunity by ID
|
|
171
|
+
- `CREATE_OPPORTUNITY` - Create a new opportunity
|
|
172
|
+
- `UPDATE_OPPORTUNITY` - Update an existing opportunity
|
|
173
|
+
- `DELETE_OPPORTUNITY` - Delete an opportunity
|
|
174
|
+
|
|
175
|
+
### Note Operations
|
|
176
|
+
- `LIST_NOTES` - List all notes
|
|
177
|
+
- `CREATE_NOTE` - Create a note attached to a record
|
|
178
|
+
|
|
179
|
+
### Task Operations
|
|
180
|
+
- `LIST_TASKS` - List all tasks
|
|
181
|
+
- `CREATE_TASK` - Create a new task
|
|
182
|
+
- `UPDATE_TASK` - Update an existing task
|
|
183
|
+
- `COMPLETE_TASK` - Mark a task as complete
|
|
184
|
+
|
|
185
|
+
### Submit Answer
|
|
186
|
+
- `SUBMIT_ANSWER` - Submit the final answer and end the session
|
|
187
|
+
|
|
188
|
+
## Integration with Training Frameworks
|
|
189
|
+
|
|
190
|
+
### Collecting Rollouts for RL Training
|
|
191
|
+
|
|
192
|
+
The SDK is designed for reinforcement learning. Here's how to collect rollouts with rewards:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from dataclasses import dataclass, field
|
|
196
|
+
from construct_labs_crm_env import CrmAgentEnv, CrmAgentObservation
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class Rollout:
|
|
200
|
+
"""A single episode rollout for training."""
|
|
201
|
+
observations: list[CrmAgentObservation] = field(default_factory=list)
|
|
202
|
+
actions: list[dict] = field(default_factory=list) # Raw tool calls
|
|
203
|
+
rewards: list[float] = field(default_factory=list)
|
|
204
|
+
done: bool = False
|
|
205
|
+
total_reward: float = 0.0
|
|
206
|
+
|
|
207
|
+
def collect_rollout(env: CrmAgentEnv, agent, seed: int | None = None) -> Rollout:
|
|
208
|
+
"""Collect a single rollout from the environment."""
|
|
209
|
+
rollout = Rollout()
|
|
210
|
+
|
|
211
|
+
# Reset environment
|
|
212
|
+
result = env.reset(seed=seed)
|
|
213
|
+
rollout.observations.append(result.observation)
|
|
214
|
+
|
|
215
|
+
while not result.done:
|
|
216
|
+
# Get action from agent (returns tool call dict)
|
|
217
|
+
tool_call = agent.get_action(
|
|
218
|
+
system_prompt=env.system_prompt,
|
|
219
|
+
tools=env.tools,
|
|
220
|
+
observation=result.observation,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Parse and execute
|
|
224
|
+
parsed = env.parse_tool_call(tool_call)
|
|
225
|
+
|
|
226
|
+
if parsed.is_valid:
|
|
227
|
+
result = env.step(parsed.action)
|
|
228
|
+
reward = result.reward if result.reward is not None else 0.0
|
|
229
|
+
else:
|
|
230
|
+
# Invalid action penalty
|
|
231
|
+
reward = -1.0
|
|
232
|
+
result.done = True
|
|
233
|
+
|
|
234
|
+
# Store transition
|
|
235
|
+
rollout.actions.append(tool_call)
|
|
236
|
+
rollout.rewards.append(reward)
|
|
237
|
+
rollout.observations.append(result.observation)
|
|
238
|
+
|
|
239
|
+
rollout.done = True
|
|
240
|
+
rollout.total_reward = sum(rollout.rewards)
|
|
241
|
+
return rollout
|
|
242
|
+
|
|
243
|
+
# Collect multiple rollouts for training
|
|
244
|
+
def collect_rollouts(
|
|
245
|
+
env: CrmAgentEnv,
|
|
246
|
+
agent,
|
|
247
|
+
num_rollouts: int,
|
|
248
|
+
seed_offset: int = 0,
|
|
249
|
+
) -> list[Rollout]:
|
|
250
|
+
"""Collect multiple rollouts for batch training."""
|
|
251
|
+
rollouts = []
|
|
252
|
+
for i in range(num_rollouts):
|
|
253
|
+
rollout = collect_rollout(env, agent, seed=seed_offset + i)
|
|
254
|
+
rollouts.append(rollout)
|
|
255
|
+
return rollouts
|
|
256
|
+
|
|
257
|
+
# Example usage
|
|
258
|
+
with CrmAgentEnv(
|
|
259
|
+
base_url="https://api.construct-labs.com",
|
|
260
|
+
api_key="your-api-key"
|
|
261
|
+
) as env:
|
|
262
|
+
# Collect 10 rollouts
|
|
263
|
+
rollouts = collect_rollouts(env, your_agent, num_rollouts=10)
|
|
264
|
+
|
|
265
|
+
# Compute statistics
|
|
266
|
+
avg_reward = sum(r.total_reward for r in rollouts) / len(rollouts)
|
|
267
|
+
avg_length = sum(len(r.actions) for r in rollouts) / len(rollouts)
|
|
268
|
+
|
|
269
|
+
print(f"Average reward: {avg_reward:.2f}")
|
|
270
|
+
print(f"Average episode length: {avg_length:.1f}")
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### GRPO Training Integration
|
|
274
|
+
|
|
275
|
+
For Group Relative Policy Optimization (GRPO) training:
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
from construct_labs_crm_env import CrmAgentEnv
|
|
279
|
+
|
|
280
|
+
def collect_grpo_group(
|
|
281
|
+
env: CrmAgentEnv,
|
|
282
|
+
agent,
|
|
283
|
+
group_size: int = 8,
|
|
284
|
+
seed: int = 0,
|
|
285
|
+
) -> list[Rollout]:
|
|
286
|
+
"""Collect a group of rollouts with the same seed for GRPO."""
|
|
287
|
+
group = []
|
|
288
|
+
for _ in range(group_size):
|
|
289
|
+
# Same seed = same initial state, different agent samples
|
|
290
|
+
rollout = collect_rollout(env, agent, seed=seed)
|
|
291
|
+
group.append(rollout)
|
|
292
|
+
return group
|
|
293
|
+
|
|
294
|
+
def compute_grpo_advantages(group: list[Rollout]) -> list[float]:
|
|
295
|
+
"""Compute relative advantages within a group."""
|
|
296
|
+
rewards = [r.total_reward for r in group]
|
|
297
|
+
mean_reward = sum(rewards) / len(rewards)
|
|
298
|
+
std_reward = (sum((r - mean_reward) ** 2 for r in rewards) / len(rewards)) ** 0.5
|
|
299
|
+
|
|
300
|
+
if std_reward < 1e-8:
|
|
301
|
+
return [0.0] * len(rewards)
|
|
302
|
+
|
|
303
|
+
return [(r - mean_reward) / std_reward for r in rewards]
|
|
304
|
+
|
|
305
|
+
# Training loop
|
|
306
|
+
with CrmAgentEnv(
|
|
307
|
+
base_url="https://api.construct-labs.com",
|
|
308
|
+
api_key="your-api-key"
|
|
309
|
+
) as env:
|
|
310
|
+
for step in range(num_training_steps):
|
|
311
|
+
# Collect group of rollouts
|
|
312
|
+
group = collect_grpo_group(env, agent, group_size=8, seed=step)
|
|
313
|
+
|
|
314
|
+
# Compute advantages
|
|
315
|
+
advantages = compute_grpo_advantages(group)
|
|
316
|
+
|
|
317
|
+
# Update policy using advantages
|
|
318
|
+
agent.update(group, advantages)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Basic Training Loop
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from construct_labs_crm_env import CrmAgentEnv
|
|
325
|
+
|
|
326
|
+
env = CrmAgentEnv(
|
|
327
|
+
base_url="https://api.construct-labs.com",
|
|
328
|
+
api_key="your-api-key"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
with env:
|
|
332
|
+
result = env.reset(seed=42)
|
|
333
|
+
|
|
334
|
+
while not result.done:
|
|
335
|
+
# Get action from your agent/LLM
|
|
336
|
+
tool_call = your_agent.get_action(
|
|
337
|
+
env.system_prompt,
|
|
338
|
+
env.tools,
|
|
339
|
+
result.observation
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Parse and execute
|
|
343
|
+
parsed = env.parse_tool_call(tool_call)
|
|
344
|
+
if parsed.is_valid:
|
|
345
|
+
result = env.step(parsed.action)
|
|
346
|
+
else:
|
|
347
|
+
# Handle invalid action
|
|
348
|
+
print(f"Invalid action: {parsed.error_message}")
|
|
349
|
+
break
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## API Reference
|
|
353
|
+
|
|
354
|
+
### CrmAgentEnv
|
|
355
|
+
|
|
356
|
+
Main client class for interacting with the CRM environment.
|
|
357
|
+
|
|
358
|
+
**Constructor:**
|
|
359
|
+
- `base_url` (str): Base URL of the CRM environment server
|
|
360
|
+
- `api_key` (str, optional): API key for authentication
|
|
361
|
+
- `connect_timeout_s` (float): Connection timeout in seconds (default: 10)
|
|
362
|
+
- `message_timeout_s` (float): Message timeout in seconds (default: 60)
|
|
363
|
+
|
|
364
|
+
**Methods:**
|
|
365
|
+
- `reset(seed=None)` - Reset the environment
|
|
366
|
+
- `step(action)` - Execute an action
|
|
367
|
+
- `state()` - Get current environment state
|
|
368
|
+
- `close()` - Close the connection
|
|
369
|
+
- `parse_tool_call(tool_call)` - Parse LLM tool call to action
|
|
370
|
+
- `format_observation(observation)` - Format observation for LLM
|
|
371
|
+
|
|
372
|
+
**Properties (overridable):**
|
|
373
|
+
- `system_prompt` - System prompt for the agent
|
|
374
|
+
- `tools` - Available tool definitions
|
|
375
|
+
|
|
376
|
+
### CrmAgentAction
|
|
377
|
+
|
|
378
|
+
Pydantic model for CRM actions.
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
action = CrmAgentAction(
|
|
382
|
+
action_type=CRMActionType.CREATE_COMPANY,
|
|
383
|
+
company_name="Acme Corp",
|
|
384
|
+
company_domain="acme.com",
|
|
385
|
+
company_employees=100
|
|
386
|
+
)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### CrmAgentObservation
|
|
390
|
+
|
|
391
|
+
Pydantic model for CRM observations.
|
|
392
|
+
|
|
393
|
+
- `success` (bool): Whether the action succeeded
|
|
394
|
+
- `error` (str | None): Error message if failed
|
|
395
|
+
- `data` (dict): Raw response data
|
|
396
|
+
- `done` (bool): Whether episode has ended
|
|
397
|
+
- `reward` (float | None): Reward signal
|
|
398
|
+
|
|
399
|
+
Use `observation.model_dump_json()` to get JSON representation.
|
|
400
|
+
|
|
401
|
+
## Support
|
|
402
|
+
|
|
403
|
+
For licensing, technical support, or questions:
|
|
404
|
+
|
|
405
|
+
**Email:** hello@construct-labs.com
|
|
406
|
+
|
|
407
|
+
## License
|
|
408
|
+
|
|
409
|
+
Copyright (c) 2024 Construct Labs GmbH. All rights reserved.
|
|
410
|
+
|
|
411
|
+
This software is proprietary and requires a commercial license.
|
|
412
|
+
See [LICENSE](LICENSE) for details.
|