construct-labs-crm-env 0.1.7__py3-none-any.whl → 0.1.9__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.
@@ -8,7 +8,7 @@ Example:
8
8
  >>> from construct_labs_crm_env import CrmAgentEnv, CrmAgentAction, CRMActionType
9
9
  >>>
10
10
  >>> with CrmAgentEnv(
11
- ... base_url="https://api.construct-labs.com",
11
+ ... base_url="https://env.crm.construct-labs.com",
12
12
  ... api_key="your-api-key"
13
13
  ... ) as env:
14
14
  ... result = env.reset()
@@ -61,7 +61,7 @@ class CrmAgentEnv(EnvClient[CrmAgentAction, CrmAgentObservation, CrmAgentState])
61
61
  Example:
62
62
  >>> # Basic usage
63
63
  >>> with CrmAgentEnv(
64
- ... base_url="https://api.construct-labs.com",
64
+ ... base_url="https://env.crm.construct-labs.com",
65
65
  ... api_key="cl_live_xxx"
66
66
  ... ) as env:
67
67
  ... result = env.reset()
@@ -0,0 +1 @@
1
+ """Example scripts for construct-labs-crm-env."""
@@ -0,0 +1,504 @@
1
+ """
2
+ Interactive chat CLI for interacting with the CRM environment via an LLM.
3
+
4
+ This example demonstrates how to build a conversational agent that uses
5
+ the CRM environment tools. The LLM can make multiple tool calls to query
6
+ and modify CRM data before providing a final answer.
7
+
8
+ Requirements:
9
+ pip install construct-labs-crm-env google-genai python-dotenv
10
+
11
+ Usage:
12
+ export CRM_AGENT_API_KEY=your-crm-api-key
13
+ export GOOGLE_API_KEY=your-google-api-key
14
+ python chat.py --help
15
+ python chat.py
16
+ python chat.py --question "How many companies are there?"
17
+ python chat.py --model gemini-2.0-flash --stream
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import sys
26
+
27
+ try:
28
+ from dotenv import load_dotenv
29
+ except ImportError:
30
+ load_dotenv = None # type: ignore[assignment, misc]
31
+
32
+ try:
33
+ from google import genai
34
+ from google.genai import types
35
+ except ImportError:
36
+ genai = None # type: ignore[assignment]
37
+ types = None # type: ignore[assignment]
38
+
39
+ from construct_labs_crm_env import CrmAgentEnv
40
+
41
+
42
+ def _check_dependencies() -> None:
43
+ """Check that optional dependencies are installed."""
44
+ missing = []
45
+ if genai is None:
46
+ missing.append("google-genai")
47
+ if load_dotenv is None:
48
+ missing.append("python-dotenv")
49
+
50
+ if missing:
51
+ print(
52
+ f"Missing dependencies: {', '.join(missing)}\n"
53
+ f"Install with: pip install construct-labs-crm-env[chat]"
54
+ )
55
+ sys.exit(1)
56
+
57
+
58
+ if load_dotenv is not None:
59
+ load_dotenv()
60
+
61
+
62
+ def print_colored(text: str, color: str) -> None:
63
+ """Print colored text to terminal."""
64
+ colors = {
65
+ "red": "\033[91m",
66
+ "green": "\033[92m",
67
+ "yellow": "\033[93m",
68
+ "blue": "\033[94m",
69
+ "magenta": "\033[95m",
70
+ "cyan": "\033[96m",
71
+ "white": "\033[97m",
72
+ "reset": "\033[0m",
73
+ "bold": "\033[1m",
74
+ "dim": "\033[2m",
75
+ }
76
+ print(f"{colors.get(color, '')}{text}{colors['reset']}")
77
+
78
+
79
+ def print_separator(char: str = "-", width: int = 60) -> None:
80
+ """Print a separator line."""
81
+ print(char * width)
82
+
83
+
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
+ def format_tool_call_compact(tool_name: str, tool_args: dict) -> str:
109
+ """Format a tool call in compact function-call style: func_name(arg1, arg2, ...)"""
110
+ if not tool_args:
111
+ return f"{tool_name}()"
112
+
113
+ arg_parts = []
114
+ for key, value in tool_args.items():
115
+ if isinstance(value, str):
116
+ if len(value) > 30:
117
+ value = value[:27] + "..."
118
+ arg_parts.append(f'{key}="{value}"')
119
+ elif isinstance(value, (dict, list)):
120
+ arg_parts.append(f"{key}=<{type(value).__name__}>")
121
+ else:
122
+ arg_parts.append(f"{key}={value}")
123
+
124
+ return f"{tool_name}({', '.join(arg_parts)})"
125
+
126
+
127
+ def _stream_response(
128
+ client: genai.Client,
129
+ model_name: str,
130
+ contents: list[types.Content],
131
+ config: types.GenerateContentConfig,
132
+ print_output: bool = True,
133
+ ) -> tuple[list[str], list[types.FunctionCall], types.Content | None]:
134
+ """
135
+ Make a streaming call to the model, optionally printing tokens as they arrive.
136
+
137
+ Returns:
138
+ Tuple of (text_parts, function_calls, final_content)
139
+ """
140
+ text_parts: list[str] = []
141
+ function_calls: list[types.FunctionCall] = []
142
+ all_parts: list[types.Part] = []
143
+
144
+ for chunk in client.models.generate_content_stream(
145
+ model=model_name,
146
+ contents=contents,
147
+ config=config,
148
+ ):
149
+ if not chunk.candidates:
150
+ continue
151
+
152
+ candidate = chunk.candidates[0]
153
+
154
+ for part in candidate.content.parts:
155
+ if part.text:
156
+ if print_output:
157
+ print(part.text, end="", flush=True)
158
+ text_parts.append(part.text)
159
+ all_parts.append(part)
160
+
161
+ if part.function_call:
162
+ function_calls.append(part.function_call)
163
+ all_parts.append(part)
164
+
165
+ if print_output and text_parts:
166
+ print()
167
+
168
+ if not all_parts:
169
+ return [], [], None
170
+
171
+ final_content = types.Content(role="model", parts=all_parts)
172
+ full_text = "".join(text_parts)
173
+ text_result = [full_text] if full_text else []
174
+
175
+ return text_result, function_calls, final_content
176
+
177
+
178
+ def _non_stream_response(
179
+ client: genai.Client,
180
+ model_name: str,
181
+ contents: list[types.Content],
182
+ config: types.GenerateContentConfig,
183
+ ) -> tuple[list[str], list[types.FunctionCall], types.Content | None]:
184
+ """Make a non-streaming call to the model."""
185
+ response = client.models.generate_content(
186
+ model=model_name,
187
+ contents=contents,
188
+ config=config,
189
+ )
190
+
191
+ if not response.candidates:
192
+ return [], [], None
193
+
194
+ candidate = response.candidates[0]
195
+ text_parts: list[str] = []
196
+ function_calls: list[types.FunctionCall] = []
197
+
198
+ for part in candidate.content.parts:
199
+ if part.text:
200
+ text_parts.append(part.text)
201
+ if part.function_call:
202
+ function_calls.append(part.function_call)
203
+
204
+ return text_parts, function_calls, candidate.content
205
+
206
+
207
+ def run_agent_loop(
208
+ env: CrmAgentEnv,
209
+ client: genai.Client,
210
+ model_name: str,
211
+ tools: list[types.Tool],
212
+ system_instruction: str,
213
+ contents: list[types.Content],
214
+ max_iterations: int = 20,
215
+ verbose: bool = False,
216
+ stream: bool = False,
217
+ ) -> tuple[str | None, list[types.Content]]:
218
+ """
219
+ Run the agent loop until it calls submit_answer or reaches max iterations.
220
+
221
+ The agent can make multiple tool calls, getting results back each time,
222
+ until it's ready to submit a final answer.
223
+
224
+ Returns:
225
+ Tuple of (final_answer or None, updated contents)
226
+ """
227
+ iteration = 0
228
+
229
+ while iteration < max_iterations:
230
+ iteration += 1
231
+
232
+ config = types.GenerateContentConfig(
233
+ tools=tools,
234
+ system_instruction=system_instruction,
235
+ temperature=0.7,
236
+ )
237
+
238
+ try:
239
+ if stream:
240
+ text_parts, function_calls, final_content = _stream_response(
241
+ client=client,
242
+ model_name=model_name,
243
+ contents=contents,
244
+ config=config,
245
+ print_output=False,
246
+ )
247
+ else:
248
+ text_parts, function_calls, final_content = _non_stream_response(
249
+ client=client,
250
+ model_name=model_name,
251
+ contents=contents,
252
+ config=config,
253
+ )
254
+ except Exception as e:
255
+ print_colored(f"API Error: {e}", "red")
256
+ return None, contents
257
+
258
+ if final_content is None:
259
+ print_colored("Error: No response from model", "red")
260
+ return None, contents
261
+
262
+ contents.append(final_content)
263
+
264
+ if not function_calls:
265
+ if text_parts:
266
+ return "\n".join(text_parts), contents
267
+ return None, contents
268
+
269
+ if text_parts:
270
+ print_colored("\nThinking:", "dim")
271
+ print("\n".join(text_parts))
272
+
273
+ function_response_parts = []
274
+ final_answer = None
275
+
276
+ for func_call in function_calls:
277
+ tool_name = func_call.name
278
+ tool_args = dict(func_call.args) if func_call.args else {}
279
+
280
+ if tool_name == "submit_answer":
281
+ final_answer = tool_args.get("answer", "")
282
+ print_colored("\n>>> Agent submitting answer", "cyan")
283
+ function_response_parts.append(
284
+ types.Part.from_function_response(
285
+ name=func_call.name,
286
+ response={"result": "Answer submitted successfully."},
287
+ )
288
+ )
289
+ continue
290
+
291
+ tool_display = format_tool_call_compact(tool_name, tool_args)
292
+ print_colored(f"\n{tool_display}", "magenta")
293
+ if verbose:
294
+ print_colored(f" Args: {json.dumps(tool_args, indent=2)}", "dim")
295
+
296
+ parsed_tool_call = {"name": tool_name, "arguments": tool_args}
297
+ parsed_action = env.parse_tool_call(parsed_tool_call)
298
+
299
+ if not parsed_action.is_valid:
300
+ obs_text = f"Error: {parsed_action.error_message}"
301
+ print_colored(f" Invalid: {parsed_action.error_message}", "red")
302
+ else:
303
+ try:
304
+ result = env.step(parsed_action.action)
305
+ obs_text = env.format_observation(result.observation)
306
+
307
+ display_text = (
308
+ obs_text[:500] + "..." if len(obs_text) > 500 else obs_text
309
+ )
310
+ print_colored(f" Result: {display_text}", "green")
311
+ except Exception as e:
312
+ obs_text = f"Error executing action: {e}"
313
+ print_colored(f" {obs_text}", "red")
314
+
315
+ function_response_parts.append(
316
+ types.Part.from_function_response(
317
+ name=func_call.name,
318
+ response={"result": obs_text},
319
+ )
320
+ )
321
+
322
+ contents.append(types.Content(role="user", parts=function_response_parts))
323
+
324
+ if final_answer is not None:
325
+ return final_answer, contents
326
+
327
+ print_colored(f"\nMax iterations ({max_iterations}) reached", "yellow")
328
+ return None, contents
329
+
330
+
331
+ def run_chat(
332
+ env: CrmAgentEnv,
333
+ client: genai.Client,
334
+ model_name: str,
335
+ tools: list[types.Tool],
336
+ system_instruction: str,
337
+ initial_question: str | None = None,
338
+ max_iterations: int = 20,
339
+ verbose: bool = False,
340
+ stream: bool = False,
341
+ ) -> None:
342
+ """
343
+ Run an interactive chat session with the environment.
344
+
345
+ The flow is:
346
+ 1. User asks a question
347
+ 2. Agent uses tools as needed (multiple calls in a loop)
348
+ 3. Agent calls submit_answer when ready
349
+ 4. Answer is displayed to user
350
+ 5. User can ask another question (loop back to 1)
351
+ """
352
+ print_colored("\nResetting environment...", "dim")
353
+ env.reset()
354
+ print_colored("Environment ready.\n", "green")
355
+
356
+ contents: list[types.Content] = []
357
+
358
+ while True:
359
+ if initial_question:
360
+ question = initial_question
361
+ initial_question = None
362
+ else:
363
+ print_colored("\nYou: ", "cyan")
364
+ try:
365
+ question = input().strip()
366
+ except EOFError:
367
+ break
368
+
369
+ if not question:
370
+ continue
371
+ if question.lower() in ("quit", "exit", "q"):
372
+ print_colored("Goodbye!", "yellow")
373
+ break
374
+
375
+ print_separator("=")
376
+
377
+ contents.append(
378
+ types.Content(role="user", parts=[types.Part.from_text(text=question)])
379
+ )
380
+
381
+ answer, contents = run_agent_loop(
382
+ env=env,
383
+ client=client,
384
+ model_name=model_name,
385
+ tools=tools,
386
+ system_instruction=system_instruction,
387
+ contents=contents,
388
+ max_iterations=max_iterations,
389
+ verbose=verbose,
390
+ stream=stream,
391
+ )
392
+
393
+ print_separator("=")
394
+ if answer:
395
+ print_colored("\nAssistant:", "blue")
396
+ print(answer)
397
+ else:
398
+ print_colored("\nNo answer provided.", "yellow")
399
+ print()
400
+
401
+
402
+ def main() -> None:
403
+ """Main entry point for the chat CLI."""
404
+ _check_dependencies()
405
+
406
+ # load_dotenv already called at module level if available
407
+
408
+ parser = argparse.ArgumentParser(
409
+ description="Interactive chat CLI for the CRM environment",
410
+ formatter_class=argparse.RawDescriptionHelpFormatter,
411
+ epilog="""
412
+ Examples:
413
+ python chat.py
414
+ python chat.py --stream
415
+ python chat.py --question "How many companies are there?"
416
+ python chat.py --model gemini-2.0-flash --stream
417
+ """,
418
+ )
419
+
420
+ parser.add_argument(
421
+ "--base-url",
422
+ type=str,
423
+ default="https://env.crm.construct-labs.com",
424
+ help="Base URL for the CRM environment (default: https://env.crm.construct-labs.com)",
425
+ )
426
+ parser.add_argument(
427
+ "--model",
428
+ type=str,
429
+ default="gemini-2.0-flash",
430
+ help="Gemini model to use (default: gemini-2.0-flash)",
431
+ )
432
+ parser.add_argument(
433
+ "--api-key",
434
+ type=str,
435
+ default=None,
436
+ help="Google API key (default: uses GOOGLE_API_KEY env var)",
437
+ )
438
+ parser.add_argument(
439
+ "--question",
440
+ type=str,
441
+ default=None,
442
+ help="Initial question/task for the agent",
443
+ )
444
+ parser.add_argument(
445
+ "--max-iterations",
446
+ type=int,
447
+ default=20,
448
+ help="Maximum tool call iterations per question (default: 20)",
449
+ )
450
+ parser.add_argument(
451
+ "--verbose",
452
+ action="store_true",
453
+ help="Print verbose output",
454
+ )
455
+ parser.add_argument(
456
+ "--stream",
457
+ action="store_true",
458
+ help="Stream tokens from the LLM as they are generated",
459
+ )
460
+
461
+ args = parser.parse_args()
462
+
463
+ api_key = args.api_key or os.getenv("GOOGLE_API_KEY")
464
+ if not api_key:
465
+ print_colored("Error: No Google API key provided.", "red")
466
+ print_colored("Set GOOGLE_API_KEY in .env file or use --api-key", "dim")
467
+ sys.exit(1)
468
+
469
+ client = genai.Client(api_key=api_key)
470
+
471
+ print_colored(f"Connecting to CRM environment at {args.base_url}...", "dim")
472
+ try:
473
+ env = CrmAgentEnv(base_url=args.base_url)
474
+ except Exception as e:
475
+ print_colored(f"Error connecting to environment: {e}", "red")
476
+ sys.exit(1)
477
+
478
+ gemini_tools = convert_openai_tools_to_gemini(env.tools)
479
+
480
+ system_prompt = """You are a helpful assistant with access to CRM tools.
481
+
482
+ Use the provided tools to help the user with their requests. You can make multiple tool calls to complete a task.
483
+
484
+ When you have gathered enough information or completed the task, call submit_answer with your response to the user."""
485
+
486
+ try:
487
+ with env:
488
+ run_chat(
489
+ env=env,
490
+ client=client,
491
+ model_name=args.model,
492
+ tools=gemini_tools,
493
+ system_instruction=system_prompt,
494
+ initial_question=args.question,
495
+ max_iterations=args.max_iterations,
496
+ verbose=args.verbose,
497
+ stream=args.stream,
498
+ )
499
+ except KeyboardInterrupt:
500
+ print_colored("\n\nSession interrupted.", "yellow")
501
+
502
+
503
+ if __name__ == "__main__":
504
+ main()
@@ -486,14 +486,35 @@ LIST_NOTES: ToolDefinition = {
486
486
  "type": "function",
487
487
  "function": {
488
488
  "name": "list_notes",
489
- "description": "List all notes in the CRM. Notes are attached to companies, people, or opportunities and contain meeting summaries, call logs, and updates.",
489
+ "description": "List all notes in the CRM. Notes are attached to companies, people, or opportunities and contain meeting summaries, call logs, and updates. Response includes pageInfo with hasNextPage, startCursor, and endCursor for pagination.",
490
490
  "parameters": {
491
491
  "type": "object",
492
492
  "properties": {
493
493
  "limit": {
494
494
  "type": "integer",
495
- "default": 10,
496
- "description": "Maximum number of notes to return. Default is 10. Use higher values to see more history.",
495
+ "default": 60,
496
+ "description": "Maximum number of notes to return (1-200). Default is 60, max is 200.",
497
+ },
498
+ "starting_after": {
499
+ "type": "string",
500
+ "description": "Cursor for forward pagination - returns notes after this cursor. Use endCursor from previous response's pageInfo.",
501
+ },
502
+ "ending_before": {
503
+ "type": "string",
504
+ "description": "Cursor for backward pagination - returns notes before this cursor. Use startCursor from previous response's pageInfo.",
505
+ },
506
+ "order_by": {
507
+ "type": "string",
508
+ "description": "Sort order in format 'field[ASC|DESC]'. Examples: 'createdAt[DESC]', 'updatedAt[ASC]'.",
509
+ },
510
+ "filter": {
511
+ "type": "string",
512
+ "description": "Filter in format 'field[comparator]:value'. Comparators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, containsAny. Quote strings/dates, not numbers. Examples: 'body[ilike]:\"%meeting%\"', 'createdAt[gte]:\"2026-01-01\"'. Use dot notation for related entities: 'company.name[ilike]:\"%acme%\"', 'person.email[eq]:\"john@example.com\"'.",
513
+ },
514
+ "depth": {
515
+ "type": "integer",
516
+ "default": 1,
517
+ "description": "Relation depth: 0 returns only note fields, 1 includes related company/person/opportunity data. Default is 1.",
497
518
  },
498
519
  },
499
520
  "required": [],
@@ -536,14 +557,35 @@ LIST_TASKS: ToolDefinition = {
536
557
  "type": "function",
537
558
  "function": {
538
559
  "name": "list_tasks",
539
- "description": "List all tasks in the CRM. Tasks represent follow-ups, reminders, and action items that may be linked to companies, people, or opportunities.",
560
+ "description": "List all tasks in the CRM. Tasks represent follow-ups, reminders, and action items that may be linked to companies, people, or opportunities. Response includes pageInfo with hasNextPage, startCursor, and endCursor for pagination.",
540
561
  "parameters": {
541
562
  "type": "object",
542
563
  "properties": {
543
564
  "limit": {
544
565
  "type": "integer",
545
- "default": 10,
546
- "description": "Maximum number of tasks to return. Default is 10. Use higher values to see more tasks.",
566
+ "default": 60,
567
+ "description": "Maximum number of tasks to return (1-200). Default is 60, max is 200.",
568
+ },
569
+ "starting_after": {
570
+ "type": "string",
571
+ "description": "Cursor for forward pagination - returns tasks after this cursor. Use endCursor from previous response's pageInfo.",
572
+ },
573
+ "ending_before": {
574
+ "type": "string",
575
+ "description": "Cursor for backward pagination - returns tasks before this cursor. Use startCursor from previous response's pageInfo.",
576
+ },
577
+ "order_by": {
578
+ "type": "string",
579
+ "description": "Sort order in format 'field[ASC|DESC]'. Examples: 'dueAt[ASC]', 'createdAt[DESC]', 'status[ASC]'.",
580
+ },
581
+ "filter": {
582
+ "type": "string",
583
+ "description": "Filter in format 'field[comparator]:value'. Comparators: eq, neq, gt, gte, lt, lte, in, is, like, ilike, startsWith, containsAny. Quote strings/dates, not numbers. Examples: 'status[eq]:\"TODO\"', 'dueAt[lte]:\"2026-02-15\"', 'title[ilike]:\"%follow up%\"'. Use dot notation for related entities.",
584
+ },
585
+ "depth": {
586
+ "type": "integer",
587
+ "default": 1,
588
+ "description": "Relation depth: 0 returns only task fields, 1 includes related company/person/opportunity data. Default is 1.",
547
589
  },
548
590
  },
549
591
  "required": [],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: construct-labs-crm-env
3
- Version: 0.1.7
3
+ Version: 0.1.9
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>
@@ -22,6 +22,9 @@ Requires-Python: >=3.10
22
22
  Requires-Dist: openenv-core>=0.2.0
23
23
  Requires-Dist: pydantic>=2.0.0
24
24
  Requires-Dist: websockets>=12.0
25
+ Provides-Extra: chat
26
+ Requires-Dist: google-genai>=1.0.0; extra == 'chat'
27
+ Requires-Dist: python-dotenv>=1.0.0; extra == 'chat'
25
28
  Provides-Extra: dev
26
29
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
27
30
  Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
@@ -41,7 +44,20 @@ Contact hello@construct-labs.com for licensing inquiries.
41
44
  ## Installation
42
45
 
43
46
  ```bash
44
- pip install construct-labs-crm-env
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
+ # Google API key (https://ai.google.dev/gemini-api/docs/api-key)
56
+ export GOOGLE_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
45
61
  ```
46
62
 
47
63
  ## Quick Start
@@ -51,7 +67,7 @@ from construct_labs_crm_env import CrmAgentEnv, CrmAgentAction, CRMActionType
51
67
 
52
68
  # Connect to the CRM environment
53
69
  with CrmAgentEnv(
54
- base_url="https://api.construct-labs.com",
70
+ base_url="https://env.crm.construct-labs.com",
55
71
  api_key="your-api-key" # Issued by Construct Labs
56
72
  ) as env:
57
73
  # Reset the environment
@@ -75,7 +91,7 @@ export CRM_AGENT_API_KEY=your-api-key
75
91
 
76
92
  ```python
77
93
  # API key is read from environment
78
- env = CrmAgentEnv(base_url="https://api.construct-labs.com")
94
+ env = CrmAgentEnv(base_url="https://env.crm.construct-labs.com")
79
95
  ```
80
96
 
81
97
  ## LLM Integration Example
@@ -86,7 +102,7 @@ The SDK is designed to work with LLM-based agents. Here's how to parse LLM tool
86
102
  from construct_labs_crm_env import CrmAgentEnv
87
103
 
88
104
  with CrmAgentEnv(
89
- base_url="https://api.construct-labs.com",
105
+ base_url="https://env.crm.construct-labs.com",
90
106
  api_key="your-api-key"
91
107
  ) as env:
92
108
  result = env.reset()
@@ -257,7 +273,7 @@ def collect_rollouts(
257
273
 
258
274
  # Example usage
259
275
  with CrmAgentEnv(
260
- base_url="https://api.construct-labs.com",
276
+ base_url="https://env.crm.construct-labs.com",
261
277
  api_key="your-api-key"
262
278
  ) as env:
263
279
  # Collect 10 rollouts
@@ -305,7 +321,7 @@ def compute_grpo_advantages(group: list[Rollout]) -> list[float]:
305
321
 
306
322
  # Training loop
307
323
  with CrmAgentEnv(
308
- base_url="https://api.construct-labs.com",
324
+ base_url="https://env.crm.construct-labs.com",
309
325
  api_key="your-api-key"
310
326
  ) as env:
311
327
  for step in range(num_training_steps):
@@ -325,7 +341,7 @@ with CrmAgentEnv(
325
341
  from construct_labs_crm_env import CrmAgentEnv
326
342
 
327
343
  env = CrmAgentEnv(
328
- base_url="https://api.construct-labs.com",
344
+ base_url="https://env.crm.construct-labs.com",
329
345
  api_key="your-api-key"
330
346
  )
331
347
 
@@ -0,0 +1,13 @@
1
+ construct_labs_crm_env/__init__.py,sha256=XRei6wERXV6MMV168AkgeAMlsZLRSdXA2pPg8icfWQ4,917
2
+ construct_labs_crm_env/client.py,sha256=XditChA2dwzdw6xJ6DjxaWbwZ5PXFJAMV4moNwngbUM,21681
3
+ construct_labs_crm_env/models.py,sha256=bs-n9FcWcR8DqKvurmm0wUebWBWp9hG6Zaj6s7jEptg,9200
4
+ construct_labs_crm_env/protocol.py,sha256=h_7-0XaV9gBNHiSXoW4aITSD9K21R1Swc5yZR6LF50A,671
5
+ construct_labs_crm_env/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ construct_labs_crm_env/tools.py,sha256=X0nrbamk2mjRvdDupLR_E_b3NUh90dYtU4bA6ydZf34,32225
7
+ construct_labs_crm_env/examples/__init__.py,sha256=cvlN7EhBvf73Df4NhAjAQZ_MVBwPjQEVq90TzfO1q9A,50
8
+ construct_labs_crm_env/examples/chat.py,sha256=NjaSK2-Bn0ccMDQZLlLO1nPB08H9ed8jFY7XBbYxqms,15178
9
+ construct_labs_crm_env-0.1.9.dist-info/METADATA,sha256=49EQHAoSh0h-e_ScyO6jCwJ4mfvf1La7uOmTThzYn28,12407
10
+ construct_labs_crm_env-0.1.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
+ construct_labs_crm_env-0.1.9.dist-info/entry_points.txt,sha256=XwAyFOckOED5VzLohiiWTvI01QPfvcUt8n9g0_dlrFM,73
12
+ construct_labs_crm_env-0.1.9.dist-info/licenses/LICENSE,sha256=zPT_KqeG9QTE0zTfKGheMHluFJB1fn_bNgNehnnpXGM,2210
13
+ construct_labs_crm_env-0.1.9.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ chat-agent = construct_labs_crm_env.examples.chat:main
@@ -1,10 +0,0 @@
1
- construct_labs_crm_env/__init__.py,sha256=XRei6wERXV6MMV168AkgeAMlsZLRSdXA2pPg8icfWQ4,917
2
- construct_labs_crm_env/client.py,sha256=aqixV5860Xkh4pSLVOGoXTDVJ6jxENOthtlrxQc3DFo,21673
3
- construct_labs_crm_env/models.py,sha256=bs-n9FcWcR8DqKvurmm0wUebWBWp9hG6Zaj6s7jEptg,9200
4
- construct_labs_crm_env/protocol.py,sha256=h_7-0XaV9gBNHiSXoW4aITSD9K21R1Swc5yZR6LF50A,671
5
- construct_labs_crm_env/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- construct_labs_crm_env/tools.py,sha256=dvW8O8fp2gCXGm3hMv-7Nd9mu4d6eUV7Q9ropuDKu_o,29237
7
- construct_labs_crm_env-0.1.7.dist-info/METADATA,sha256=b3fzLHEnIr31iJznIrZISG0iX5SokOqyNDcmjywyt5w,11923
8
- construct_labs_crm_env-0.1.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
9
- construct_labs_crm_env-0.1.7.dist-info/licenses/LICENSE,sha256=zPT_KqeG9QTE0zTfKGheMHluFJB1fn_bNgNehnnpXGM,2210
10
- construct_labs_crm_env-0.1.7.dist-info/RECORD,,