construct-labs-crm-env 0.1.8__tar.gz → 0.1.10__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.8 → construct_labs_crm_env-0.1.10}/PKG-INFO +16 -3
- {construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/README.md +14 -1
- {construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/pyproject.toml +2 -2
- {construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/src/construct_labs_crm_env/examples/chat.py +163 -155
- {construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/.gitignore +0 -0
- {construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/LICENSE +0 -0
- {construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/src/construct_labs_crm_env/__init__.py +0 -0
- {construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/src/construct_labs_crm_env/client.py +0 -0
- {construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/src/construct_labs_crm_env/examples/__init__.py +0 -0
- {construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/src/construct_labs_crm_env/models.py +0 -0
- {construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/src/construct_labs_crm_env/protocol.py +0 -0
- {construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/src/construct_labs_crm_env/py.typed +0 -0
- {construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/src/construct_labs_crm_env/tools.py +0 -0
|
@@ -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.10
|
|
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>
|
|
@@ -23,7 +23,7 @@ Requires-Dist: openenv-core>=0.2.0
|
|
|
23
23
|
Requires-Dist: pydantic>=2.0.0
|
|
24
24
|
Requires-Dist: websockets>=12.0
|
|
25
25
|
Provides-Extra: chat
|
|
26
|
-
Requires-Dist:
|
|
26
|
+
Requires-Dist: openai>=1.0.0; extra == 'chat'
|
|
27
27
|
Requires-Dist: python-dotenv>=1.0.0; extra == 'chat'
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
@@ -44,7 +44,20 @@ Contact hello@construct-labs.com for licensing inquiries.
|
|
|
44
44
|
## Installation
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
|
|
47
|
+
uv add construct-labs-crm-env
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Try the Example Chat Agent
|
|
51
|
+
|
|
52
|
+
Run the interactive chat agent without installing:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# OpenRouter API key (https://openrouter.ai/keys)
|
|
56
|
+
export OPENAI_API_KEY=<api_key>
|
|
57
|
+
# Construct Labs CRM API Key (provided by Construct Labs)
|
|
58
|
+
export CRM_AGENT_API_KEY=<api_key>
|
|
59
|
+
|
|
60
|
+
uvx --from construct-labs-crm-env[chat] chat-agent
|
|
48
61
|
```
|
|
49
62
|
|
|
50
63
|
## Quick Start
|
|
@@ -10,7 +10,20 @@ Contact hello@construct-labs.com for licensing inquiries.
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
|
|
13
|
+
uv add construct-labs-crm-env
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Try the Example Chat Agent
|
|
17
|
+
|
|
18
|
+
Run the interactive chat agent without installing:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# OpenRouter API key (https://openrouter.ai/keys)
|
|
22
|
+
export OPENAI_API_KEY=<api_key>
|
|
23
|
+
# Construct Labs CRM API Key (provided by Construct Labs)
|
|
24
|
+
export CRM_AGENT_API_KEY=<api_key>
|
|
25
|
+
|
|
26
|
+
uvx --from construct-labs-crm-env[chat] chat-agent
|
|
14
27
|
```
|
|
15
28
|
|
|
16
29
|
## Quick Start
|
|
@@ -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.10"
|
|
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" }
|
|
@@ -44,7 +44,7 @@ chat-agent = "construct_labs_crm_env.examples.chat:main"
|
|
|
44
44
|
|
|
45
45
|
[project.optional-dependencies]
|
|
46
46
|
chat = [
|
|
47
|
-
"
|
|
47
|
+
"openai>=1.0.0",
|
|
48
48
|
"python-dotenv>=1.0.0",
|
|
49
49
|
]
|
|
50
50
|
dev = [
|
|
@@ -6,15 +6,18 @@ the CRM environment tools. The LLM can make multiple tool calls to query
|
|
|
6
6
|
and modify CRM data before providing a final answer.
|
|
7
7
|
|
|
8
8
|
Requirements:
|
|
9
|
-
pip install construct-labs-crm-env
|
|
9
|
+
pip install construct-labs-crm-env openai python-dotenv
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
12
|
export CRM_AGENT_API_KEY=your-crm-api-key
|
|
13
|
-
export
|
|
13
|
+
export OPENAI_API_KEY=your-openrouter-api-key
|
|
14
14
|
python chat.py --help
|
|
15
15
|
python chat.py
|
|
16
16
|
python chat.py --question "How many companies are there?"
|
|
17
|
-
python chat.py --model
|
|
17
|
+
python chat.py --model anthropic/claude-sonnet-4 --stream
|
|
18
|
+
|
|
19
|
+
# Using OpenAI directly:
|
|
20
|
+
python chat.py --llm-base-url https://api.openai.com/v1 --model gpt-4o
|
|
18
21
|
"""
|
|
19
22
|
|
|
20
23
|
from __future__ import annotations
|
|
@@ -23,6 +26,7 @@ import argparse
|
|
|
23
26
|
import json
|
|
24
27
|
import os
|
|
25
28
|
import sys
|
|
29
|
+
from typing import Any
|
|
26
30
|
|
|
27
31
|
try:
|
|
28
32
|
from dotenv import load_dotenv
|
|
@@ -30,11 +34,9 @@ except ImportError:
|
|
|
30
34
|
load_dotenv = None # type: ignore[assignment, misc]
|
|
31
35
|
|
|
32
36
|
try:
|
|
33
|
-
from
|
|
34
|
-
from google.genai import types
|
|
37
|
+
from openai import OpenAI
|
|
35
38
|
except ImportError:
|
|
36
|
-
|
|
37
|
-
types = None # type: ignore[assignment]
|
|
39
|
+
OpenAI = None # type: ignore[assignment, misc]
|
|
38
40
|
|
|
39
41
|
from construct_labs_crm_env import CrmAgentEnv
|
|
40
42
|
|
|
@@ -42,8 +44,8 @@ from construct_labs_crm_env import CrmAgentEnv
|
|
|
42
44
|
def _check_dependencies() -> None:
|
|
43
45
|
"""Check that optional dependencies are installed."""
|
|
44
46
|
missing = []
|
|
45
|
-
if
|
|
46
|
-
missing.append("
|
|
47
|
+
if OpenAI is None:
|
|
48
|
+
missing.append("openai")
|
|
47
49
|
if load_dotenv is None:
|
|
48
50
|
missing.append("python-dotenv")
|
|
49
51
|
|
|
@@ -81,30 +83,6 @@ def print_separator(char: str = "-", width: int = 60) -> None:
|
|
|
81
83
|
print(char * width)
|
|
82
84
|
|
|
83
85
|
|
|
84
|
-
def convert_openai_tools_to_gemini(openai_tools: list[dict]) -> list[types.Tool]:
|
|
85
|
-
"""Convert OpenAI-format tools to Gemini format."""
|
|
86
|
-
function_declarations = []
|
|
87
|
-
|
|
88
|
-
for tool in openai_tools:
|
|
89
|
-
if tool.get("type") != "function":
|
|
90
|
-
continue
|
|
91
|
-
|
|
92
|
-
func = tool["function"]
|
|
93
|
-
name = func["name"]
|
|
94
|
-
description = func.get("description", "")
|
|
95
|
-
parameters = func.get("parameters", {})
|
|
96
|
-
|
|
97
|
-
function_declarations.append(
|
|
98
|
-
types.FunctionDeclaration(
|
|
99
|
-
name=name,
|
|
100
|
-
description=description,
|
|
101
|
-
parameters=parameters if parameters else None,
|
|
102
|
-
)
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
return [types.Tool(function_declarations=function_declarations)]
|
|
106
|
-
|
|
107
|
-
|
|
108
86
|
def format_tool_call_compact(tool_name: str, tool_args: dict) -> str:
|
|
109
87
|
"""Format a tool call in compact function-call style: func_name(arg1, arg2, ...)"""
|
|
110
88
|
if not tool_args:
|
|
@@ -125,96 +103,130 @@ def format_tool_call_compact(tool_name: str, tool_args: dict) -> str:
|
|
|
125
103
|
|
|
126
104
|
|
|
127
105
|
def _stream_response(
|
|
128
|
-
client:
|
|
106
|
+
client: OpenAI,
|
|
129
107
|
model_name: str,
|
|
130
|
-
|
|
131
|
-
|
|
108
|
+
messages: list[dict[str, Any]],
|
|
109
|
+
tools: list[dict[str, Any]],
|
|
132
110
|
print_output: bool = True,
|
|
133
|
-
) -> tuple[list[str],
|
|
111
|
+
) -> tuple[str | None, list[dict[str, Any]], dict[str, Any] | None]:
|
|
134
112
|
"""
|
|
135
113
|
Make a streaming call to the model, optionally printing tokens as they arrive.
|
|
136
114
|
|
|
137
115
|
Returns:
|
|
138
|
-
Tuple of (
|
|
116
|
+
Tuple of (text_content, tool_calls, assistant_message)
|
|
139
117
|
"""
|
|
140
118
|
text_parts: list[str] = []
|
|
141
|
-
|
|
142
|
-
all_parts: list[types.Part] = []
|
|
119
|
+
tool_calls_by_index: dict[int, dict[str, Any]] = {}
|
|
143
120
|
|
|
144
|
-
|
|
121
|
+
stream = client.chat.completions.create(
|
|
145
122
|
model=model_name,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
candidate = chunk.candidates[0]
|
|
123
|
+
messages=messages,
|
|
124
|
+
tools=tools,
|
|
125
|
+
temperature=0.7,
|
|
126
|
+
stream=True,
|
|
127
|
+
)
|
|
153
128
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
print(part.text, end="", flush=True)
|
|
158
|
-
text_parts.append(part.text)
|
|
159
|
-
all_parts.append(part)
|
|
129
|
+
for chunk in stream:
|
|
130
|
+
if not chunk.choices:
|
|
131
|
+
continue
|
|
160
132
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
133
|
+
delta = chunk.choices[0].delta
|
|
134
|
+
|
|
135
|
+
if delta.content:
|
|
136
|
+
if print_output:
|
|
137
|
+
print(delta.content, end="", flush=True)
|
|
138
|
+
text_parts.append(delta.content)
|
|
139
|
+
|
|
140
|
+
if delta.tool_calls:
|
|
141
|
+
for tc in delta.tool_calls:
|
|
142
|
+
idx = tc.index
|
|
143
|
+
if idx not in tool_calls_by_index:
|
|
144
|
+
tool_calls_by_index[idx] = {
|
|
145
|
+
"id": tc.id or "",
|
|
146
|
+
"type": "function",
|
|
147
|
+
"function": {"name": "", "arguments": ""},
|
|
148
|
+
}
|
|
149
|
+
if tc.id:
|
|
150
|
+
tool_calls_by_index[idx]["id"] = tc.id
|
|
151
|
+
if tc.function:
|
|
152
|
+
if tc.function.name:
|
|
153
|
+
tool_calls_by_index[idx]["function"]["name"] = tc.function.name
|
|
154
|
+
if tc.function.arguments:
|
|
155
|
+
tool_calls_by_index[idx]["function"][
|
|
156
|
+
"arguments"
|
|
157
|
+
] += tc.function.arguments
|
|
164
158
|
|
|
165
159
|
if print_output and text_parts:
|
|
166
160
|
print()
|
|
167
161
|
|
|
168
|
-
if
|
|
169
|
-
|
|
162
|
+
text_content = "".join(text_parts) if text_parts else None
|
|
163
|
+
tool_calls = [tool_calls_by_index[i] for i in sorted(tool_calls_by_index.keys())]
|
|
170
164
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
text_result = [full_text] if full_text else []
|
|
165
|
+
if not text_content and not tool_calls:
|
|
166
|
+
return None, [], None
|
|
174
167
|
|
|
175
|
-
|
|
168
|
+
assistant_message: dict[str, Any] = {"role": "assistant"}
|
|
169
|
+
if text_content:
|
|
170
|
+
assistant_message["content"] = text_content
|
|
171
|
+
if tool_calls:
|
|
172
|
+
assistant_message["tool_calls"] = tool_calls
|
|
173
|
+
|
|
174
|
+
return text_content, tool_calls, assistant_message
|
|
176
175
|
|
|
177
176
|
|
|
178
177
|
def _non_stream_response(
|
|
179
|
-
client:
|
|
178
|
+
client: OpenAI,
|
|
180
179
|
model_name: str,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
) -> tuple[list[str],
|
|
180
|
+
messages: list[dict[str, Any]],
|
|
181
|
+
tools: list[dict[str, Any]],
|
|
182
|
+
) -> tuple[str | None, list[dict[str, Any]], dict[str, Any] | None]:
|
|
184
183
|
"""Make a non-streaming call to the model."""
|
|
185
|
-
response = client.
|
|
184
|
+
response = client.chat.completions.create(
|
|
186
185
|
model=model_name,
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
messages=messages,
|
|
187
|
+
tools=tools,
|
|
188
|
+
temperature=0.7,
|
|
189
189
|
)
|
|
190
190
|
|
|
191
|
-
if not response.
|
|
192
|
-
return
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
191
|
+
if not response.choices:
|
|
192
|
+
return None, [], None
|
|
193
|
+
|
|
194
|
+
message = response.choices[0].message
|
|
195
|
+
text_content = message.content
|
|
196
|
+
tool_calls = []
|
|
197
|
+
|
|
198
|
+
if message.tool_calls:
|
|
199
|
+
for tc in message.tool_calls:
|
|
200
|
+
tool_calls.append(
|
|
201
|
+
{
|
|
202
|
+
"id": tc.id,
|
|
203
|
+
"type": "function",
|
|
204
|
+
"function": {
|
|
205
|
+
"name": tc.function.name,
|
|
206
|
+
"arguments": tc.function.arguments,
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
)
|
|
197
210
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
211
|
+
assistant_message: dict[str, Any] = {"role": "assistant"}
|
|
212
|
+
if text_content:
|
|
213
|
+
assistant_message["content"] = text_content
|
|
214
|
+
if tool_calls:
|
|
215
|
+
assistant_message["tool_calls"] = tool_calls
|
|
203
216
|
|
|
204
|
-
return
|
|
217
|
+
return text_content, tool_calls, assistant_message
|
|
205
218
|
|
|
206
219
|
|
|
207
220
|
def run_agent_loop(
|
|
208
221
|
env: CrmAgentEnv,
|
|
209
|
-
client:
|
|
222
|
+
client: OpenAI,
|
|
210
223
|
model_name: str,
|
|
211
|
-
tools: list[
|
|
212
|
-
|
|
213
|
-
contents: list[types.Content],
|
|
224
|
+
tools: list[dict[str, Any]],
|
|
225
|
+
messages: list[dict[str, Any]],
|
|
214
226
|
max_iterations: int = 20,
|
|
215
227
|
verbose: bool = False,
|
|
216
228
|
stream: bool = False,
|
|
217
|
-
) -> tuple[str | None, list[
|
|
229
|
+
) -> tuple[str | None, list[dict[str, Any]]]:
|
|
218
230
|
"""
|
|
219
231
|
Run the agent loop until it calls submit_answer or reaches max iterations.
|
|
220
232
|
|
|
@@ -222,69 +234,64 @@ def run_agent_loop(
|
|
|
222
234
|
until it's ready to submit a final answer.
|
|
223
235
|
|
|
224
236
|
Returns:
|
|
225
|
-
Tuple of (final_answer or None, updated
|
|
237
|
+
Tuple of (final_answer or None, updated messages)
|
|
226
238
|
"""
|
|
227
239
|
iteration = 0
|
|
228
240
|
|
|
229
241
|
while iteration < max_iterations:
|
|
230
242
|
iteration += 1
|
|
231
243
|
|
|
232
|
-
config = types.GenerateContentConfig(
|
|
233
|
-
tools=tools,
|
|
234
|
-
system_instruction=system_instruction,
|
|
235
|
-
temperature=0.7,
|
|
236
|
-
)
|
|
237
|
-
|
|
238
244
|
try:
|
|
239
245
|
if stream:
|
|
240
|
-
|
|
246
|
+
text_content, tool_calls, assistant_message = _stream_response(
|
|
241
247
|
client=client,
|
|
242
248
|
model_name=model_name,
|
|
243
|
-
|
|
244
|
-
|
|
249
|
+
messages=messages,
|
|
250
|
+
tools=tools,
|
|
245
251
|
print_output=False,
|
|
246
252
|
)
|
|
247
253
|
else:
|
|
248
|
-
|
|
254
|
+
text_content, tool_calls, assistant_message = _non_stream_response(
|
|
249
255
|
client=client,
|
|
250
256
|
model_name=model_name,
|
|
251
|
-
|
|
252
|
-
|
|
257
|
+
messages=messages,
|
|
258
|
+
tools=tools,
|
|
253
259
|
)
|
|
254
260
|
except Exception as e:
|
|
255
261
|
print_colored(f"API Error: {e}", "red")
|
|
256
|
-
return None,
|
|
262
|
+
return None, messages
|
|
257
263
|
|
|
258
|
-
if
|
|
264
|
+
if assistant_message is None:
|
|
259
265
|
print_colored("Error: No response from model", "red")
|
|
260
|
-
return None,
|
|
266
|
+
return None, messages
|
|
261
267
|
|
|
262
|
-
|
|
268
|
+
messages.append(assistant_message)
|
|
263
269
|
|
|
264
|
-
if not
|
|
265
|
-
|
|
266
|
-
return "\n".join(text_parts), contents
|
|
267
|
-
return None, contents
|
|
270
|
+
if not tool_calls:
|
|
271
|
+
return text_content, messages
|
|
268
272
|
|
|
269
|
-
if
|
|
273
|
+
if text_content:
|
|
270
274
|
print_colored("\nThinking:", "dim")
|
|
271
|
-
print(
|
|
275
|
+
print(text_content)
|
|
272
276
|
|
|
273
|
-
function_response_parts = []
|
|
274
277
|
final_answer = None
|
|
275
278
|
|
|
276
|
-
for
|
|
277
|
-
tool_name =
|
|
278
|
-
|
|
279
|
+
for tc in tool_calls:
|
|
280
|
+
tool_name = tc["function"]["name"]
|
|
281
|
+
try:
|
|
282
|
+
tool_args = json.loads(tc["function"]["arguments"])
|
|
283
|
+
except json.JSONDecodeError:
|
|
284
|
+
tool_args = {}
|
|
279
285
|
|
|
280
286
|
if tool_name == "submit_answer":
|
|
281
287
|
final_answer = tool_args.get("answer", "")
|
|
282
288
|
print_colored("\n>>> Agent submitting answer", "cyan")
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
289
|
+
messages.append(
|
|
290
|
+
{
|
|
291
|
+
"role": "tool",
|
|
292
|
+
"tool_call_id": tc["id"],
|
|
293
|
+
"content": "Answer submitted successfully.",
|
|
294
|
+
}
|
|
288
295
|
)
|
|
289
296
|
continue
|
|
290
297
|
|
|
@@ -312,28 +319,27 @@ def run_agent_loop(
|
|
|
312
319
|
obs_text = f"Error executing action: {e}"
|
|
313
320
|
print_colored(f" {obs_text}", "red")
|
|
314
321
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
322
|
+
messages.append(
|
|
323
|
+
{
|
|
324
|
+
"role": "tool",
|
|
325
|
+
"tool_call_id": tc["id"],
|
|
326
|
+
"content": obs_text,
|
|
327
|
+
}
|
|
320
328
|
)
|
|
321
329
|
|
|
322
|
-
contents.append(types.Content(role="user", parts=function_response_parts))
|
|
323
|
-
|
|
324
330
|
if final_answer is not None:
|
|
325
|
-
return final_answer,
|
|
331
|
+
return final_answer, messages
|
|
326
332
|
|
|
327
333
|
print_colored(f"\nMax iterations ({max_iterations}) reached", "yellow")
|
|
328
|
-
return None,
|
|
334
|
+
return None, messages
|
|
329
335
|
|
|
330
336
|
|
|
331
337
|
def run_chat(
|
|
332
338
|
env: CrmAgentEnv,
|
|
333
|
-
client:
|
|
339
|
+
client: OpenAI,
|
|
334
340
|
model_name: str,
|
|
335
|
-
tools: list[
|
|
336
|
-
|
|
341
|
+
tools: list[dict[str, Any]],
|
|
342
|
+
system_prompt: str,
|
|
337
343
|
initial_question: str | None = None,
|
|
338
344
|
max_iterations: int = 20,
|
|
339
345
|
verbose: bool = False,
|
|
@@ -353,7 +359,7 @@ def run_chat(
|
|
|
353
359
|
env.reset()
|
|
354
360
|
print_colored("Environment ready.\n", "green")
|
|
355
361
|
|
|
356
|
-
|
|
362
|
+
messages: list[dict[str, Any]] = [{"role": "system", "content": system_prompt}]
|
|
357
363
|
|
|
358
364
|
while True:
|
|
359
365
|
if initial_question:
|
|
@@ -374,17 +380,14 @@ def run_chat(
|
|
|
374
380
|
|
|
375
381
|
print_separator("=")
|
|
376
382
|
|
|
377
|
-
|
|
378
|
-
types.Content(role="user", parts=[types.Part.from_text(text=question)])
|
|
379
|
-
)
|
|
383
|
+
messages.append({"role": "user", "content": question})
|
|
380
384
|
|
|
381
|
-
answer,
|
|
385
|
+
answer, messages = run_agent_loop(
|
|
382
386
|
env=env,
|
|
383
387
|
client=client,
|
|
384
388
|
model_name=model_name,
|
|
385
389
|
tools=tools,
|
|
386
|
-
|
|
387
|
-
contents=contents,
|
|
390
|
+
messages=messages,
|
|
388
391
|
max_iterations=max_iterations,
|
|
389
392
|
verbose=verbose,
|
|
390
393
|
stream=stream,
|
|
@@ -403,8 +406,6 @@ def main() -> None:
|
|
|
403
406
|
"""Main entry point for the chat CLI."""
|
|
404
407
|
_check_dependencies()
|
|
405
408
|
|
|
406
|
-
# load_dotenv already called at module level if available
|
|
407
|
-
|
|
408
409
|
parser = argparse.ArgumentParser(
|
|
409
410
|
description="Interactive chat CLI for the CRM environment",
|
|
410
411
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
@@ -413,27 +414,36 @@ Examples:
|
|
|
413
414
|
python chat.py
|
|
414
415
|
python chat.py --stream
|
|
415
416
|
python chat.py --question "How many companies are there?"
|
|
416
|
-
python chat.py --model
|
|
417
|
+
python chat.py --model anthropic/claude-sonnet-4 --stream
|
|
418
|
+
|
|
419
|
+
# Using OpenAI directly:
|
|
420
|
+
python chat.py --llm-base-url https://api.openai.com/v1 --model gpt-4o
|
|
417
421
|
""",
|
|
418
422
|
)
|
|
419
423
|
|
|
420
424
|
parser.add_argument(
|
|
421
|
-
"--base-url",
|
|
425
|
+
"--crm-base-url",
|
|
422
426
|
type=str,
|
|
423
427
|
default="https://env.crm.construct-labs.com",
|
|
424
428
|
help="Base URL for the CRM environment (default: https://env.crm.construct-labs.com)",
|
|
425
429
|
)
|
|
430
|
+
parser.add_argument(
|
|
431
|
+
"--llm-base-url",
|
|
432
|
+
type=str,
|
|
433
|
+
default="https://openrouter.ai/api/v1",
|
|
434
|
+
help="Base URL for OpenAI-compatible API (default: OpenRouter)",
|
|
435
|
+
)
|
|
426
436
|
parser.add_argument(
|
|
427
437
|
"--model",
|
|
428
438
|
type=str,
|
|
429
|
-
default="
|
|
430
|
-
help="
|
|
439
|
+
default="anthropic/claude-sonnet-4.5",
|
|
440
|
+
help="Model to use (default: anthropic/claude-sonnet-4.5)",
|
|
431
441
|
)
|
|
432
442
|
parser.add_argument(
|
|
433
443
|
"--api-key",
|
|
434
444
|
type=str,
|
|
435
445
|
default=None,
|
|
436
|
-
help="
|
|
446
|
+
help="API key (default: uses OPENAI_API_KEY env var)",
|
|
437
447
|
)
|
|
438
448
|
parser.add_argument(
|
|
439
449
|
"--question",
|
|
@@ -460,23 +470,21 @@ Examples:
|
|
|
460
470
|
|
|
461
471
|
args = parser.parse_args()
|
|
462
472
|
|
|
463
|
-
api_key = args.api_key or os.getenv("
|
|
473
|
+
api_key = args.api_key or os.getenv("OPENAI_API_KEY")
|
|
464
474
|
if not api_key:
|
|
465
|
-
print_colored("Error: No
|
|
466
|
-
print_colored("Set
|
|
475
|
+
print_colored("Error: No API key provided.", "red")
|
|
476
|
+
print_colored("Set OPENAI_API_KEY in .env file or use --api-key", "dim")
|
|
467
477
|
sys.exit(1)
|
|
468
478
|
|
|
469
|
-
client =
|
|
479
|
+
client = OpenAI(api_key=api_key, base_url=args.llm_base_url)
|
|
470
480
|
|
|
471
|
-
print_colored(f"Connecting to CRM environment at {args.
|
|
481
|
+
print_colored(f"Connecting to CRM environment at {args.crm_base_url}...", "dim")
|
|
472
482
|
try:
|
|
473
|
-
env = CrmAgentEnv(base_url=args.
|
|
483
|
+
env = CrmAgentEnv(base_url=args.crm_base_url)
|
|
474
484
|
except Exception as e:
|
|
475
485
|
print_colored(f"Error connecting to environment: {e}", "red")
|
|
476
486
|
sys.exit(1)
|
|
477
487
|
|
|
478
|
-
gemini_tools = convert_openai_tools_to_gemini(env.tools)
|
|
479
|
-
|
|
480
488
|
system_prompt = """You are a helpful assistant with access to CRM tools.
|
|
481
489
|
|
|
482
490
|
Use the provided tools to help the user with their requests. You can make multiple tool calls to complete a task.
|
|
@@ -489,8 +497,8 @@ When you have gathered enough information or completed the task, call submit_ans
|
|
|
489
497
|
env=env,
|
|
490
498
|
client=client,
|
|
491
499
|
model_name=args.model,
|
|
492
|
-
tools=
|
|
493
|
-
|
|
500
|
+
tools=env.tools,
|
|
501
|
+
system_prompt=system_prompt,
|
|
494
502
|
initial_question=args.question,
|
|
495
503
|
max_iterations=args.max_iterations,
|
|
496
504
|
verbose=args.verbose,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/src/construct_labs_crm_env/client.py
RENAMED
|
File without changes
|
|
File without changes
|
{construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/src/construct_labs_crm_env/models.py
RENAMED
|
File without changes
|
|
File without changes
|
{construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/src/construct_labs_crm_env/py.typed
RENAMED
|
File without changes
|
{construct_labs_crm_env-0.1.8 → construct_labs_crm_env-0.1.10}/src/construct_labs_crm_env/tools.py
RENAMED
|
File without changes
|