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.
@@ -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.