construct-labs-crm-env 0.1.1__tar.gz → 0.1.3__tar.gz
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-0.1.3/.gitignore +23 -0
- construct_labs_crm_env-0.1.3/LICENSE +56 -0
- {construct_labs_crm_env-0.1.1 → construct_labs_crm_env-0.1.3}/PKG-INFO +2 -1
- {construct_labs_crm_env-0.1.1 → construct_labs_crm_env-0.1.3}/pyproject.toml +3 -2
- {construct_labs_crm_env-0.1.1 → construct_labs_crm_env-0.1.3}/src/construct_labs_crm_env/__init__.py +7 -2
- construct_labs_crm_env-0.1.3/src/construct_labs_crm_env/client.py +515 -0
- {construct_labs_crm_env-0.1.1 → construct_labs_crm_env-0.1.3}/src/construct_labs_crm_env/models.py +1 -2
- construct_labs_crm_env-0.1.3/src/construct_labs_crm_env/tools.py +611 -0
- construct_labs_crm_env-0.1.1/.gitignore +0 -24
- construct_labs_crm_env-0.1.1/LICENSE +0 -42
- construct_labs_crm_env-0.1.1/src/construct_labs_crm_env/_vendored/LICENSE.openenv +0 -28
- construct_labs_crm_env-0.1.1/src/construct_labs_crm_env/_vendored/__init__.py +0 -14
- construct_labs_crm_env-0.1.1/src/construct_labs_crm_env/_vendored/_client.py +0 -216
- construct_labs_crm_env-0.1.1/src/construct_labs_crm_env/_vendored/_types.py +0 -82
- construct_labs_crm_env-0.1.1/src/construct_labs_crm_env/client.py +0 -1004
- {construct_labs_crm_env-0.1.1 → construct_labs_crm_env-0.1.3}/README.md +0 -0
- {construct_labs_crm_env-0.1.1 → construct_labs_crm_env-0.1.3}/src/construct_labs_crm_env/protocol.py +0 -0
- {construct_labs_crm_env-0.1.1 → construct_labs_crm_env-0.1.3}/src/construct_labs_crm_env/py.typed +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
.venv/
|
|
6
|
+
|
|
7
|
+
# Testing
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.coverage
|
|
10
|
+
htmlcov/
|
|
11
|
+
|
|
12
|
+
# Linting
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
|
|
16
|
+
# Build
|
|
17
|
+
dist/
|
|
18
|
+
build/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
|
|
21
|
+
# IDE
|
|
22
|
+
.idea/
|
|
23
|
+
.vscode/
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Construct Labs CRM Environment SDK - Evaluation License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Construct Labs GmbH. All rights reserved.
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS
|
|
6
|
+
|
|
7
|
+
1. EVALUATION LICENSE
|
|
8
|
+
This software and associated documentation files (the "Software") are
|
|
9
|
+
provided by Construct Labs GmbH for evaluation purposes only.
|
|
10
|
+
|
|
11
|
+
You may use this Software solely to evaluate its functionality and
|
|
12
|
+
suitability for your needs. This evaluation license does not grant
|
|
13
|
+
any rights to use the Software in production environments.
|
|
14
|
+
|
|
15
|
+
2. RESTRICTIONS
|
|
16
|
+
Under this evaluation license, you may NOT:
|
|
17
|
+
- Deploy the Software in production environments
|
|
18
|
+
- Use the Software for commercial purposes
|
|
19
|
+
- Train models intended for production use
|
|
20
|
+
- Distribute, sublicense, or transfer the Software to third parties
|
|
21
|
+
- Remove or alter any proprietary notices
|
|
22
|
+
|
|
23
|
+
3. COMMERCIAL LICENSE REQUIRED
|
|
24
|
+
Any use beyond evaluation requires a commercial license agreement
|
|
25
|
+
with Construct Labs GmbH. This includes but is not limited to:
|
|
26
|
+
- Production deployment
|
|
27
|
+
- Commercial use of any kind
|
|
28
|
+
- Training models for production use
|
|
29
|
+
- Integration into commercial products or services
|
|
30
|
+
|
|
31
|
+
To obtain a commercial license, contact:
|
|
32
|
+
|
|
33
|
+
Construct Labs GmbH
|
|
34
|
+
Email: hello@construct-labs.com
|
|
35
|
+
|
|
36
|
+
4. DATA AND PRIVACY
|
|
37
|
+
During evaluation, the Software may connect to Construct Labs servers.
|
|
38
|
+
Usage data may be collected for service improvement purposes.
|
|
39
|
+
|
|
40
|
+
5. WARRANTY DISCLAIMER
|
|
41
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
42
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
43
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
44
|
+
|
|
45
|
+
6. LIMITATION OF LIABILITY
|
|
46
|
+
IN NO EVENT SHALL CONSTRUCT LABS GMBH BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
47
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
48
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
49
|
+
DEALINGS IN THE SOFTWARE.
|
|
50
|
+
|
|
51
|
+
7. TERMINATION
|
|
52
|
+
This evaluation license may be terminated by Construct Labs GmbH at any
|
|
53
|
+
time. Your rights under this license will terminate automatically without
|
|
54
|
+
notice if you fail to comply with any of its terms.
|
|
55
|
+
|
|
56
|
+
For licensing inquiries: hello@construct-labs.com
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: construct-labs-crm-env
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: CRM Agent Environment SDK by Construct Labs - Train RL agents to interact with CRM systems
|
|
5
5
|
Project-URL: Homepage, https://construct-labs.com
|
|
6
6
|
Author-email: Construct Labs GmbH <hello@construct-labs.com>
|
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
19
19
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
20
|
Classifier: Typing :: Typed
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: openenv-core>=0.2.0
|
|
22
23
|
Requires-Dist: pydantic>=2.0.0
|
|
23
24
|
Requires-Dist: websockets>=12.0
|
|
24
25
|
Provides-Extra: dev
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "construct-labs-crm-env"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "CRM Agent Environment SDK by Construct Labs - Train RL agents to interact with CRM systems"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "Proprietary" }
|
|
@@ -34,6 +34,7 @@ classifiers = [
|
|
|
34
34
|
"Typing :: Typed",
|
|
35
35
|
]
|
|
36
36
|
dependencies = [
|
|
37
|
+
"openenv-core>=0.2.0",
|
|
37
38
|
"pydantic>=2.0.0",
|
|
38
39
|
"websockets>=12.0",
|
|
39
40
|
]
|
|
@@ -88,7 +89,7 @@ warn_unused_ignores = true
|
|
|
88
89
|
disallow_untyped_defs = true
|
|
89
90
|
|
|
90
91
|
[[tool.mypy.overrides]]
|
|
91
|
-
module = ["websockets.*"]
|
|
92
|
+
module = ["websockets.*", "openenv.*"]
|
|
92
93
|
ignore_missing_imports = true
|
|
93
94
|
|
|
94
95
|
[tool.pytest.ini_options]
|
{construct_labs_crm_env-0.1.1 → construct_labs_crm_env-0.1.3}/src/construct_labs_crm_env/__init__.py
RENAMED
|
@@ -4,9 +4,11 @@ This package provides a Python client for interacting with the Construct Labs
|
|
|
4
4
|
CRM Agent Environment - a reinforcement learning environment for training
|
|
5
5
|
agents to interact with CRM systems.
|
|
6
6
|
|
|
7
|
-
For
|
|
7
|
+
For licensing and support, contact hello@construct-labs.com
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
11
|
+
|
|
10
12
|
from .client import CrmAgentEnv
|
|
11
13
|
from .models import (
|
|
12
14
|
CRMActionType,
|
|
@@ -16,7 +18,10 @@ from .models import (
|
|
|
16
18
|
)
|
|
17
19
|
from .protocol import ParsedAction
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
try:
|
|
22
|
+
__version__ = version("construct-labs-crm-env")
|
|
23
|
+
except PackageNotFoundError:
|
|
24
|
+
__version__ = "0.0.0+dev"
|
|
20
25
|
|
|
21
26
|
__all__ = [
|
|
22
27
|
# Main client
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""CRM Agent Environment Client.
|
|
2
|
+
|
|
3
|
+
This module provides the client for connecting to the Construct Labs CRM Agent
|
|
4
|
+
Environment. The client handles authentication, WebSocket communication, and
|
|
5
|
+
provides an extensible interface for customizing agent behavior.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from construct_labs_crm_env import CrmAgentEnv, CrmAgentAction, CRMActionType
|
|
9
|
+
>>>
|
|
10
|
+
>>> with CrmAgentEnv(
|
|
11
|
+
... base_url="https://api.construct-labs.com",
|
|
12
|
+
... api_key="your-api-key"
|
|
13
|
+
... ) as env:
|
|
14
|
+
... result = env.reset()
|
|
15
|
+
... result = env.step(CrmAgentAction(
|
|
16
|
+
... action_type=CRMActionType.LIST_COMPANIES,
|
|
17
|
+
... limit=10
|
|
18
|
+
... ))
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
from typing import Any, cast
|
|
26
|
+
|
|
27
|
+
from openenv.core import EnvClient
|
|
28
|
+
from openenv.core.client_types import StepResult
|
|
29
|
+
from websockets.sync.client import connect as ws_connect
|
|
30
|
+
|
|
31
|
+
from .models import (
|
|
32
|
+
CRMActionType,
|
|
33
|
+
CrmAgentAction,
|
|
34
|
+
CrmAgentObservation,
|
|
35
|
+
CrmAgentState,
|
|
36
|
+
)
|
|
37
|
+
from .protocol import ParsedAction
|
|
38
|
+
from .tools import DEFAULT_TOOLS
|
|
39
|
+
|
|
40
|
+
# Type alias for JSON-serializable dictionaries
|
|
41
|
+
JsonDict = dict[str, Any]
|
|
42
|
+
|
|
43
|
+
# Protocol version for API compatibility
|
|
44
|
+
_PROTOCOL_VERSION = "v1"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CrmAgentEnv(EnvClient[CrmAgentAction, CrmAgentObservation, CrmAgentState]):
|
|
48
|
+
"""Client for the Construct Labs CRM Agent Environment.
|
|
49
|
+
|
|
50
|
+
This client connects to the CRM environment server via WebSocket and
|
|
51
|
+
provides methods for interacting with CRM data. It supports customization
|
|
52
|
+
through subclassing - override `system_prompt`, `tools`, or
|
|
53
|
+
`format_observation` to customize agent behavior.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
base_url: Base URL of the CRM environment server.
|
|
57
|
+
api_key: API key for authentication. Get one at https://construct-labs.com
|
|
58
|
+
connect_timeout_s: Timeout for establishing connection (default: 10s).
|
|
59
|
+
message_timeout_s: Timeout for receiving responses (default: 60s).
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
>>> # Basic usage
|
|
63
|
+
>>> with CrmAgentEnv(
|
|
64
|
+
... base_url="https://api.construct-labs.com",
|
|
65
|
+
... api_key="cl_live_xxx"
|
|
66
|
+
... ) as env:
|
|
67
|
+
... result = env.reset()
|
|
68
|
+
... print(env.system_prompt)
|
|
69
|
+
|
|
70
|
+
Example (custom subclass):
|
|
71
|
+
>>> class SalesAgent(CrmAgentEnv):
|
|
72
|
+
... @property
|
|
73
|
+
... def system_prompt(self) -> str:
|
|
74
|
+
... return "You are a sales assistant..."
|
|
75
|
+
...
|
|
76
|
+
... @property
|
|
77
|
+
... def tools(self) -> list[dict]:
|
|
78
|
+
... # Only expose company and opportunity tools
|
|
79
|
+
... return [t for t in self._default_tools()
|
|
80
|
+
... if 'company' in t['function']['name']
|
|
81
|
+
... or 'opportunity' in t['function']['name']]
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
base_url: str,
|
|
87
|
+
api_key: str | None = None,
|
|
88
|
+
connect_timeout_s: float = 10.0,
|
|
89
|
+
message_timeout_s: float = 60.0,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Initialize the CRM environment client.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
base_url: Base URL of the CRM environment server.
|
|
95
|
+
api_key: API key for authentication. Can also be set via
|
|
96
|
+
CRM_AGENT_API_KEY environment variable.
|
|
97
|
+
connect_timeout_s: Timeout for establishing WebSocket connection.
|
|
98
|
+
message_timeout_s: Timeout for receiving responses.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ValueError: If no API key is provided or found in environment.
|
|
102
|
+
"""
|
|
103
|
+
# Resolve API key from parameter or environment
|
|
104
|
+
resolved_api_key = api_key or os.environ.get("CRM_AGENT_API_KEY")
|
|
105
|
+
if not resolved_api_key:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
"API key is required. Pass api_key parameter or set "
|
|
108
|
+
"CRM_AGENT_API_KEY environment variable. "
|
|
109
|
+
"Contact hello@construct-labs.com to obtain an API key."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
self._api_key = resolved_api_key
|
|
113
|
+
|
|
114
|
+
# Initialize parent class (but don't connect yet)
|
|
115
|
+
super().__init__(
|
|
116
|
+
base_url=base_url,
|
|
117
|
+
connect_timeout_s=connect_timeout_s,
|
|
118
|
+
message_timeout_s=message_timeout_s,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def connect(self) -> CrmAgentEnv:
|
|
122
|
+
"""Establish authenticated WebSocket connection to the server.
|
|
123
|
+
|
|
124
|
+
The API key is transmitted via WebSocket subprotocol for secure
|
|
125
|
+
authentication during the handshake.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
self for method chaining.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
ConnectionError: If connection cannot be established or
|
|
132
|
+
authentication fails.
|
|
133
|
+
"""
|
|
134
|
+
if self._ws is not None:
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
# Bypass proxy for localhost connections
|
|
138
|
+
ws_url_lower = self._ws_url.lower()
|
|
139
|
+
is_localhost = "localhost" in ws_url_lower or "127.0.0.1" in ws_url_lower
|
|
140
|
+
|
|
141
|
+
old_no_proxy = os.environ.get("NO_PROXY")
|
|
142
|
+
if is_localhost:
|
|
143
|
+
current_no_proxy = old_no_proxy or ""
|
|
144
|
+
if "localhost" not in current_no_proxy.lower():
|
|
145
|
+
os.environ["NO_PROXY"] = (
|
|
146
|
+
f"{current_no_proxy},localhost,127.0.0.1"
|
|
147
|
+
if current_no_proxy
|
|
148
|
+
else "localhost,127.0.0.1"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
# Authenticate via WebSocket subprotocol
|
|
153
|
+
# Format: crm-{version}.{api_key}
|
|
154
|
+
auth_subprotocol = f"crm-{_PROTOCOL_VERSION}.{self._api_key}"
|
|
155
|
+
|
|
156
|
+
self._ws = ws_connect(
|
|
157
|
+
self._ws_url,
|
|
158
|
+
open_timeout=self._connect_timeout,
|
|
159
|
+
subprotocols=[auth_subprotocol],
|
|
160
|
+
)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
error_msg = str(e)
|
|
163
|
+
if "401" in error_msg or "403" in error_msg or "4001" in error_msg:
|
|
164
|
+
raise ConnectionError(
|
|
165
|
+
"Authentication failed. Please verify your API key. "
|
|
166
|
+
"Contact hello@construct-labs.com if you need assistance."
|
|
167
|
+
) from e
|
|
168
|
+
raise ConnectionError(f"Failed to connect to {self._ws_url}: {e}") from e
|
|
169
|
+
finally:
|
|
170
|
+
# Restore original NO_PROXY value
|
|
171
|
+
if is_localhost:
|
|
172
|
+
if old_no_proxy is None:
|
|
173
|
+
os.environ.pop("NO_PROXY", None)
|
|
174
|
+
else:
|
|
175
|
+
os.environ["NO_PROXY"] = old_no_proxy
|
|
176
|
+
|
|
177
|
+
return self
|
|
178
|
+
|
|
179
|
+
def _step_payload(self, action: CrmAgentAction) -> JsonDict:
|
|
180
|
+
"""Convert CrmAgentAction to JSON payload for step request."""
|
|
181
|
+
return action.model_dump()
|
|
182
|
+
|
|
183
|
+
def _parse_result(self, payload: JsonDict) -> StepResult[CrmAgentObservation]:
|
|
184
|
+
"""Parse server response into StepResult."""
|
|
185
|
+
obs_data = payload.get("observation", {})
|
|
186
|
+
observation = CrmAgentObservation.model_validate(obs_data)
|
|
187
|
+
|
|
188
|
+
return StepResult(
|
|
189
|
+
observation=observation,
|
|
190
|
+
reward=payload.get("reward", observation.reward),
|
|
191
|
+
done=payload.get("done", observation.done),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def _parse_state(self, payload: JsonDict) -> CrmAgentState:
|
|
195
|
+
"""Parse server response into CrmAgentState."""
|
|
196
|
+
return CrmAgentState.model_validate(payload)
|
|
197
|
+
|
|
198
|
+
# =========================================================================
|
|
199
|
+
# Extensible Properties - Override these in subclasses
|
|
200
|
+
# =========================================================================
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def system_prompt(self) -> str:
|
|
204
|
+
"""System prompt for the CRM agent.
|
|
205
|
+
|
|
206
|
+
Override this property in a subclass to customize the agent's behavior
|
|
207
|
+
and instructions.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
The system prompt string to use for the agent.
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
>>> class CustomAgent(CrmAgentEnv):
|
|
214
|
+
... @property
|
|
215
|
+
... def system_prompt(self) -> str:
|
|
216
|
+
... return '''You are a data entry assistant.
|
|
217
|
+
... Focus on accuracy and completeness.'''
|
|
218
|
+
"""
|
|
219
|
+
return self._default_system_prompt()
|
|
220
|
+
|
|
221
|
+
def _default_system_prompt(self) -> str:
|
|
222
|
+
"""Return the default system prompt.
|
|
223
|
+
|
|
224
|
+
Subclasses can call this to get the default prompt and extend it.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
The default system prompt string.
|
|
228
|
+
"""
|
|
229
|
+
return """You are a tool-using agent interacting with a CRM (Customer Relationship Management) system.
|
|
230
|
+
|
|
231
|
+
GOAL: Complete CRM tasks by creating, updating, and managing business data.
|
|
232
|
+
|
|
233
|
+
AVAILABLE OPERATIONS:
|
|
234
|
+
- Companies: list, get, create, update, delete
|
|
235
|
+
- People/Contacts: list, get, create, update, delete
|
|
236
|
+
- Opportunities: list, get, create, update, delete
|
|
237
|
+
- Notes: list, create (attach to companies, people, or opportunities)
|
|
238
|
+
- Tasks: list, create, update, complete
|
|
239
|
+
|
|
240
|
+
EXAMPLES:
|
|
241
|
+
|
|
242
|
+
1. List companies:
|
|
243
|
+
<tool_call>
|
|
244
|
+
{"name": "list_companies", "arguments": {"limit": 10}}
|
|
245
|
+
</tool_call>
|
|
246
|
+
|
|
247
|
+
2. Create a company:
|
|
248
|
+
<tool_call>
|
|
249
|
+
{"name": "create_company", "arguments": {"company_name": "Acme Corp", "company_domain": "acme.com"}}
|
|
250
|
+
</tool_call>
|
|
251
|
+
|
|
252
|
+
3. Create a contact:
|
|
253
|
+
<tool_call>
|
|
254
|
+
{"name": "create_person", "arguments": {"person_first_name": "John", "person_last_name": "Doe", "person_email": "john@acme.com"}}
|
|
255
|
+
</tool_call>
|
|
256
|
+
|
|
257
|
+
4. Submit final answer:
|
|
258
|
+
<tool_call>
|
|
259
|
+
{"name": "submit_answer", "arguments": {"answer": "The total pipeline value is $1.5M"}}
|
|
260
|
+
</tool_call>
|
|
261
|
+
|
|
262
|
+
IMPORTANT: Output ONLY a tool_call, no other text."""
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def tools(self) -> list[JsonDict]:
|
|
266
|
+
"""Tool definitions for the CRM environment.
|
|
267
|
+
|
|
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).
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
List of tool definitions (OpenAI format by default).
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
>>> class ReadOnlyAgent(CrmAgentEnv):
|
|
277
|
+
... @property
|
|
278
|
+
... def tools(self) -> list[dict]:
|
|
279
|
+
... # Only allow read operations
|
|
280
|
+
... read_ops = {'list_', 'get_', 'submit_answer'}
|
|
281
|
+
... return [t for t in self._default_tools()
|
|
282
|
+
... if any(op in t['function']['name'] for op in read_ops)]
|
|
283
|
+
"""
|
|
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
|
|
312
|
+
|
|
313
|
+
def _default_tools(self) -> list[JsonDict]:
|
|
314
|
+
"""Return the default tool definitions.
|
|
315
|
+
|
|
316
|
+
Subclasses can call this to get all default tools and filter/extend them.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Complete list of CRM tool definitions in OpenAI format.
|
|
320
|
+
"""
|
|
321
|
+
return list(DEFAULT_TOOLS)
|
|
322
|
+
|
|
323
|
+
# =========================================================================
|
|
324
|
+
# Tool Parsing and Observation Formatting
|
|
325
|
+
# =========================================================================
|
|
326
|
+
|
|
327
|
+
def parse_tool_call(self, tool_call: JsonDict) -> ParsedAction:
|
|
328
|
+
"""Parse a tool call from the LLM into a CrmAgentAction.
|
|
329
|
+
|
|
330
|
+
This method maps tool names to action types and extracts arguments.
|
|
331
|
+
Override this in a subclass to handle custom tools.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
tool_call: Dictionary with 'name' and 'arguments' from LLM output.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
ParsedAction with the action and validity status.
|
|
338
|
+
"""
|
|
339
|
+
tool_name = str(tool_call.get("name", "")).lower().strip()
|
|
340
|
+
arguments_raw = tool_call.get("arguments")
|
|
341
|
+
|
|
342
|
+
# Parse arguments
|
|
343
|
+
if isinstance(arguments_raw, dict):
|
|
344
|
+
arguments = cast(JsonDict, arguments_raw)
|
|
345
|
+
elif isinstance(arguments_raw, str):
|
|
346
|
+
try:
|
|
347
|
+
arguments = json.loads(arguments_raw)
|
|
348
|
+
except json.JSONDecodeError:
|
|
349
|
+
return ParsedAction(
|
|
350
|
+
action=None,
|
|
351
|
+
is_valid=False,
|
|
352
|
+
error_message=f"Invalid JSON in arguments: {arguments_raw}",
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
return ParsedAction(
|
|
356
|
+
action=None,
|
|
357
|
+
is_valid=False,
|
|
358
|
+
error_message=f"Arguments must be dict or JSON string, got: {type(arguments_raw)}",
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Map tool names to action types
|
|
362
|
+
tool_to_action_type: dict[str, CRMActionType] = {
|
|
363
|
+
# Company
|
|
364
|
+
"list_companies": CRMActionType.LIST_COMPANIES,
|
|
365
|
+
"get_company": CRMActionType.GET_COMPANY,
|
|
366
|
+
"create_company": CRMActionType.CREATE_COMPANY,
|
|
367
|
+
"update_company": CRMActionType.UPDATE_COMPANY,
|
|
368
|
+
"delete_company": CRMActionType.DELETE_COMPANY,
|
|
369
|
+
# Person
|
|
370
|
+
"list_people": CRMActionType.LIST_PEOPLE,
|
|
371
|
+
"get_person": CRMActionType.GET_PERSON,
|
|
372
|
+
"create_person": CRMActionType.CREATE_PERSON,
|
|
373
|
+
"update_person": CRMActionType.UPDATE_PERSON,
|
|
374
|
+
"delete_person": CRMActionType.DELETE_PERSON,
|
|
375
|
+
# Opportunity
|
|
376
|
+
"list_opportunities": CRMActionType.LIST_OPPORTUNITIES,
|
|
377
|
+
"get_opportunity": CRMActionType.GET_OPPORTUNITY,
|
|
378
|
+
"create_opportunity": CRMActionType.CREATE_OPPORTUNITY,
|
|
379
|
+
"update_opportunity": CRMActionType.UPDATE_OPPORTUNITY,
|
|
380
|
+
"delete_opportunity": CRMActionType.DELETE_OPPORTUNITY,
|
|
381
|
+
# Note
|
|
382
|
+
"list_notes": CRMActionType.LIST_NOTES,
|
|
383
|
+
"create_note": CRMActionType.CREATE_NOTE,
|
|
384
|
+
# Task
|
|
385
|
+
"list_tasks": CRMActionType.LIST_TASKS,
|
|
386
|
+
"create_task": CRMActionType.CREATE_TASK,
|
|
387
|
+
"update_task": CRMActionType.UPDATE_TASK,
|
|
388
|
+
"complete_task": CRMActionType.COMPLETE_TASK,
|
|
389
|
+
# Answer
|
|
390
|
+
"submit_answer": CRMActionType.SUBMIT_ANSWER,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if not tool_name:
|
|
394
|
+
return ParsedAction(
|
|
395
|
+
action=None,
|
|
396
|
+
is_valid=False,
|
|
397
|
+
error_message="Tool name is required",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
action_type = tool_to_action_type.get(tool_name)
|
|
401
|
+
if action_type is None:
|
|
402
|
+
return ParsedAction(
|
|
403
|
+
action=None,
|
|
404
|
+
is_valid=False,
|
|
405
|
+
error_message=f"Unknown tool: '{tool_name}'. "
|
|
406
|
+
f"Valid tools: {list(tool_to_action_type.keys())}",
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# Build action with all valid fields
|
|
410
|
+
action_kwargs: JsonDict = {"action_type": action_type}
|
|
411
|
+
|
|
412
|
+
valid_fields = {
|
|
413
|
+
"record_id",
|
|
414
|
+
"company_name",
|
|
415
|
+
"company_domain",
|
|
416
|
+
"company_address",
|
|
417
|
+
"company_employees",
|
|
418
|
+
"person_first_name",
|
|
419
|
+
"person_last_name",
|
|
420
|
+
"person_email",
|
|
421
|
+
"person_phone",
|
|
422
|
+
"person_company_id",
|
|
423
|
+
"person_job_title",
|
|
424
|
+
"opportunity_name",
|
|
425
|
+
"opportunity_amount",
|
|
426
|
+
"opportunity_stage",
|
|
427
|
+
"opportunity_close_date",
|
|
428
|
+
"opportunity_company_id",
|
|
429
|
+
"opportunity_person_id",
|
|
430
|
+
"note_body",
|
|
431
|
+
"note_target_id",
|
|
432
|
+
"note_target_type",
|
|
433
|
+
"task_title",
|
|
434
|
+
"task_body",
|
|
435
|
+
"task_due_date",
|
|
436
|
+
"task_assignee_id",
|
|
437
|
+
"task_status",
|
|
438
|
+
"task_target_id",
|
|
439
|
+
"task_target_type",
|
|
440
|
+
"limit",
|
|
441
|
+
"cursor",
|
|
442
|
+
"starting_after",
|
|
443
|
+
"ending_before",
|
|
444
|
+
"order_by",
|
|
445
|
+
"filter",
|
|
446
|
+
"depth",
|
|
447
|
+
"answer",
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
for field in valid_fields:
|
|
451
|
+
if field in arguments and arguments[field] is not None:
|
|
452
|
+
action_kwargs[field] = arguments[field]
|
|
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
|
+
|
|
480
|
+
try:
|
|
481
|
+
action = CrmAgentAction(**action_kwargs)
|
|
482
|
+
return ParsedAction(action=action, is_valid=True)
|
|
483
|
+
except Exception as e:
|
|
484
|
+
return ParsedAction(
|
|
485
|
+
action=None,
|
|
486
|
+
is_valid=False,
|
|
487
|
+
error_message=f"Failed to create action: {e}",
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
def format_observation(self, observation: CrmAgentObservation) -> str:
|
|
491
|
+
"""Format an observation as a string for the LLM.
|
|
492
|
+
|
|
493
|
+
Override this in a subclass to customize how observations are
|
|
494
|
+
presented to the agent.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
observation: The observation to format.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
String representation for the LLM.
|
|
501
|
+
|
|
502
|
+
Example:
|
|
503
|
+
>>> class VerboseAgent(CrmAgentEnv):
|
|
504
|
+
... def format_observation(self, obs):
|
|
505
|
+
... base = super().format_observation(obs)
|
|
506
|
+
... return f"=== CRM Response ===\\n{base}\\n=== End ==="
|
|
507
|
+
"""
|
|
508
|
+
import json
|
|
509
|
+
|
|
510
|
+
lines: list[str] = [json.dumps(observation.data)]
|
|
511
|
+
|
|
512
|
+
if observation.error:
|
|
513
|
+
lines.append(f"Error: {observation.error}")
|
|
514
|
+
|
|
515
|
+
return "\n".join(lines)
|
{construct_labs_crm_env-0.1.1 → construct_labs_crm_env-0.1.3}/src/construct_labs_crm_env/models.py
RENAMED
|
@@ -9,10 +9,9 @@ from __future__ import annotations
|
|
|
9
9
|
from enum import Enum
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
+
from openenv.core import Action, Observation, State
|
|
12
13
|
from pydantic import ConfigDict, Field
|
|
13
14
|
|
|
14
|
-
from ._vendored import Action, Observation, State
|
|
15
|
-
|
|
16
15
|
|
|
17
16
|
class CRMActionType(str, Enum):
|
|
18
17
|
"""Types of actions available in the CRM environment.
|