Skip to content

Commit ae02205

Browse files
[BUZZOK-25993] Sync run_agent.py with latest changes (#1467)
* Synchronize run_agent with datarobot-user-models * Bump drum * Reconcile dependencies, updated IDs, tags --------- Co-authored-by: svc-harness-git2 <svc-harness-git2@datarobot.com>
1 parent 9e61282 commit ae02205

File tree

5 files changed

+154
-74
lines changed

5 files changed

+154
-74
lines changed

public_dropin_environments/python311_genai_agents/env_info.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "This template environment can be used to create GenAI-powered agents using CrewAI, LangGraph, or Llama-Index. Similar to other drop-in environments, you can either include a .pth artifact or any other code needed to deserialize your model, and optionally a custom.py file. You can also use this environment in codespaces.",
55
"programmingLanguage": "python",
66
"label": "",
7-
"environmentVersionId": "6830d3140012af0f96ebdc53",
7+
"environmentVersionId": "683733219604e901ac1a7541",
88
"environmentVersionDescription": "",
99
"isPublic": true,
1010
"useCases": [
@@ -13,8 +13,8 @@
1313
],
1414
"imageRepository": "env-python-genai-agents",
1515
"tags": [
16-
"v11.1.0-6830d3140012af0f96ebdc53",
17-
"6830d3140012af0f96ebdc53",
16+
"v11.1.0-683733219604e901ac1a7541",
17+
"683733219604e901ac1a7541",
1818
"v11.1.0-latest"
1919
]
2020
}

public_dropin_environments/python311_genai_agents/ipython_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
# This need to load extensions automaticaly when kernel starting
15+
# This need to load extensions automatically when kernel starting
1616

1717
c.InteractiveShellApp.extensions = ["dataframe_formatter"]

public_dropin_environments/python311_genai_agents/requirements.in

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ ecs-logging
44
jupyter-client
55
jupyter_kernel_gateway
66
jupyter_core
7-
ipykernel
7+
ipykernel<6.29.0
88
pandas
99
numpy
1010
mistune
11-
datarobot-drum @ git+https://github.com/datarobot/datarobot-user-models#egg=datarobot-drum&subdirectory=custom_model_runner
11+
datarobot-drum>=1.16.14
1212
datarobot
1313
numpy
1414
uwsgi
15+
flask>=3.1.1
1516

1617
# Generative AI Frameworks
1718
crewai

public_dropin_environments/python311_genai_agents/requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ crewai-tools==0.44.0
5050
cryptography==44.0.3
5151
dataclasses-json==0.6.7
5252
datarobot==3.7.1
53-
datarobot-drum @ git+https://github.com/datarobot/datarobot-user-models#subdirectory=custom_model_runner
53+
datarobot-drum==1.16.14
5454
datarobot-mlops==11.0.0
5555
datarobot-storage==2.2.0
5656
datasets==3.6.0
@@ -109,7 +109,7 @@ importlib-metadata==8.6.1
109109
importlib-resources==6.5.2
110110
inflection==0.5.1
111111
instructor==1.8.1
112-
ipykernel==6.29.5
112+
ipykernel==6.28.0
113113
ipython==8.36.0
114114
isodate==0.7.2
115115
isoduration==20.11.0
@@ -326,7 +326,7 @@ tinycss2==1.4.0
326326
tokenizers==0.20.3
327327
tomli==2.2.1
328328
tomli-w==1.2.0
329-
tornado==6.5.1
329+
tornado==6.5
330330
tqdm==4.67.1
331331
traceloop-sdk==0.40.3
332332
trafaret==2.1.1

public_dropin_environments/python311_genai_agents/run_agent.py

Lines changed: 144 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -17,64 +17,92 @@
1717
import logging
1818
import os
1919
import sys
20-
from typing import cast
20+
from pathlib import Path
21+
from typing import Any
22+
from urllib.parse import urlparse, urlunparse
2123

2224
import requests
2325
from datarobot_drum.drum.enum import TargetType
2426
from datarobot_drum.drum.root_predictors.drum_server_utils import DrumServerRun
2527
from openai import OpenAI
2628
from openai.types.chat import ChatCompletion
27-
28-
root = logging.getLogger()
29-
30-
parser = argparse.ArgumentParser()
31-
parser.add_argument(
32-
"--chat_completion",
33-
type=str,
34-
default="{}",
35-
help="OpenAI ChatCompletion dict as json string",
36-
)
37-
parser.add_argument(
38-
"--default_headers",
39-
type=str,
40-
default="{}",
41-
help="OpenAI default_headers as json string",
42-
)
43-
parser.add_argument(
44-
"--custom_model_dir",
45-
type=str,
46-
default="",
47-
help="directory containing custom.py location",
29+
from openai.types.chat.completion_create_params import (
30+
CompletionCreateParamsBase,
4831
)
49-
parser.add_argument("--output_path", type=str, default="", help="json output file location")
50-
args = parser.parse_args()
32+
from pydantic import TypeAdapter
5133

34+
root = logging.getLogger()
5235

53-
def setup_logging(logger: logging.Logger, output_path: str, log_level: int = logging.INFO) -> None:
54-
if len(output_path) == 0:
55-
output_path = "output.log"
56-
else:
57-
output_path = f"{output_path}.log"
58-
36+
CURRENT_DIR = Path(__file__).parent
37+
DEFAULT_OUTPUT_LOG_PATH = CURRENT_DIR / "output.log"
38+
DEFAULT_OUTPUT_JSON_PATH = CURRENT_DIR / "output.json"
39+
40+
41+
def argparse_args() -> argparse.Namespace:
42+
parser = argparse.ArgumentParser()
43+
parser.add_argument(
44+
"--chat_completion",
45+
type=str,
46+
required=True,
47+
help="OpenAI ChatCompletion dict as json string",
48+
)
49+
parser.add_argument(
50+
"--default_headers",
51+
type=str,
52+
default="{}",
53+
help="OpenAI default_headers as json string",
54+
)
55+
parser.add_argument(
56+
"--custom_model_dir",
57+
type=str,
58+
required=True,
59+
help="directory containing custom.py location",
60+
)
61+
parser.add_argument("--output_path", type=str, default=None, help="json output file location")
62+
parser.add_argument("--otlp_entity_id", type=str, default=None, help="Entity ID for tracing")
63+
args = parser.parse_args()
64+
return args
65+
66+
67+
def setup_logging(logger: logging.Logger, log_level: int = logging.INFO) -> None:
5968
logger.setLevel(log_level)
6069
handler_stream = logging.StreamHandler(sys.stdout)
6170
handler_stream.setLevel(log_level)
62-
formatter = logging.Formatter("%(message)s")
71+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
6372
handler_stream.setFormatter(formatter)
6473

65-
if os.path.exists(output_path):
66-
os.remove(output_path)
67-
handler_file = logging.FileHandler(output_path)
68-
handler_file.setLevel(log_level)
69-
formatter_file = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
70-
handler_file.setFormatter(formatter_file)
71-
7274
logger.addHandler(handler_stream)
73-
logger.addHandler(handler_file)
75+
76+
77+
def setup_otlp_env_variables(entity_id: str | None = None) -> None:
78+
# do not override if already set
79+
if os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") or os.environ.get(
80+
"OTEL_EXPORTER_OTLP_HEADERS"
81+
):
82+
root.info("OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_HEADERS already set, skipping")
83+
return
84+
85+
datarobot_endpoint = os.environ.get("DATAROBOT_ENDPOINT")
86+
datarobot_api_token = os.environ.get("DATAROBOT_API_TOKEN")
87+
if not datarobot_endpoint or not datarobot_api_token:
88+
root.warning("DATAROBOT_ENDPOINT or DATAROBOT_API_TOKEN not set, tracing is disabled")
89+
return
90+
91+
parsed_url = urlparse(datarobot_endpoint)
92+
stripped_url = (parsed_url.scheme, parsed_url.netloc, "otel", "", "", "")
93+
otlp_endpoint = urlunparse(stripped_url)
94+
otlp_headers = f"X-DataRobot-Api-Key={datarobot_api_token}"
95+
if entity_id:
96+
otlp_headers += f",X-DataRobot-Entity-Id={entity_id}"
97+
os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = otlp_endpoint
98+
os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = otlp_headers
99+
root.info(f"Using OTEL_EXPORTER_OTLP_ENDPOINT: {otlp_endpoint}")
74100

75101

76102
def execute_drum(
77-
chat_completion: str, default_headers: str, custom_model_dir: str, output_path: str
103+
chat_completion: CompletionCreateParamsBase,
104+
default_headers: dict[str, str],
105+
custom_model_dir: Path,
78106
) -> ChatCompletion:
79107
root.info("Executing agent as [chat] endpoint. DRUM Executor.")
80108
root.info("Starting DRUM server.")
@@ -97,44 +125,95 @@ def execute_drum(
97125
root.error("Server failed to start")
98126
try:
99127
root.error(response.text)
100-
root.error(response.json())
101128
finally:
102129
raise RuntimeError("Server failed to start")
103130

104-
root.info("Parsing OpenAI request")
105-
completion_params = json.loads(chat_completion)
106-
header_params = json.loads(default_headers)
107-
108131
# Use a standard OpenAI client to call the DRUM server. This mirrors the behavior of a deployed agent.
109132
# Using the `chat.completions.create` method ensures the parameters are OpenAI compatible.
110133
root.info("Executing Agent")
111134
client = OpenAI(
112135
base_url=drum_runner.url_server_address,
113136
api_key="not-required",
114-
default_headers=header_params,
137+
default_headers=default_headers,
115138
max_retries=0,
116139
)
117-
completion = client.chat.completions.create(**completion_params)
118-
140+
completion = client.chat.completions.create(**chat_completion)
119141
# Continue outside the context manager to ensure the server is stopped and logs
120142
# are flushed before we write the output
121-
root.info(f"Storing result: {output_path}")
122-
if len(output_path) == 0:
123-
output_path = os.path.join(custom_model_dir, "output.json")
124-
with open(output_path, "w") as fp:
125-
fp.write(completion.to_json())
143+
return completion
126144

127-
root.info(completion.to_json())
128-
return cast(ChatCompletion, completion)
129145

146+
def construct_prompt(chat_completion: str) -> CompletionCreateParamsBase:
147+
chat_completion_dict = json.loads(chat_completion)
148+
if "model" not in chat_completion_dict:
149+
chat_completion_dict["model"] = "unknown"
150+
validator = TypeAdapter(CompletionCreateParamsBase)
151+
validator.validate_python(chat_completion_dict)
152+
completion_create_params: CompletionCreateParamsBase = CompletionCreateParamsBase(
153+
**chat_completion_dict # type: ignore[typeddict-item]
154+
)
155+
return completion_create_params
130156

131-
# Agent execution
132-
if len(args.custom_model_dir) == 0:
133-
args.custom_model_dir = os.path.join(os.getcwd(), "custom_model")
134-
setup_logging(logger=root, output_path=args.output_path, log_level=logging.INFO)
135-
result = execute_drum(
136-
chat_completion=args.chat_completion,
137-
default_headers=args.default_headers,
138-
custom_model_dir=args.custom_model_dir,
139-
output_path=args.output_path,
140-
)
157+
158+
def store_result(result: ChatCompletion, output_path: Path) -> None:
159+
root.info(f"Storing result: {output_path}")
160+
with open(output_path, "w") as fp:
161+
fp.write(result.to_json())
162+
163+
164+
def main() -> Any:
165+
with open(DEFAULT_OUTPUT_LOG_PATH, "w") as f:
166+
sys.stdout = f
167+
sys.stderr = f
168+
print("Parsing args")
169+
args = argparse_args()
170+
171+
output_log_path = (
172+
Path(args.output_path + ".log") if args.output_path else DEFAULT_OUTPUT_LOG_PATH
173+
)
174+
with open(output_log_path, "a") as f:
175+
sys.stdout = f
176+
sys.stderr = f
177+
178+
try:
179+
print("Setting up logging")
180+
setup_logging(logger=root, log_level=logging.INFO)
181+
root.info("Parsing args")
182+
183+
# Parse input to fail early if it's not valid
184+
chat_completion = construct_prompt(args.chat_completion)
185+
default_headers = json.loads(args.default_headers)
186+
root.info(f"Chat completion: {chat_completion}")
187+
root.info(f"Default headers: {default_headers}")
188+
189+
# Setup tracing
190+
print("Setting up tracing")
191+
setup_otlp_env_variables(args.otlp_entity_id)
192+
193+
root.info(f"Executing request in directory {args.custom_model_dir}")
194+
result = execute_drum(
195+
chat_completion=chat_completion,
196+
default_headers=default_headers,
197+
custom_model_dir=args.custom_model_dir,
198+
)
199+
root.info(f"Result: {result}")
200+
store_result(
201+
result,
202+
Path(args.output_path) if args.output_path else DEFAULT_OUTPUT_JSON_PATH,
203+
)
204+
except Exception as e:
205+
root.exception(f"Error executing agent: {e}")
206+
207+
208+
if __name__ == "__main__":
209+
stdout = sys.stdout
210+
stderr = sys.stderr
211+
try:
212+
main()
213+
except Exception:
214+
pass
215+
finally:
216+
# Return to original stdout and stderr otherwise the kernel will fail to flush and
217+
# hang
218+
sys.stdout = stdout
219+
sys.stderr = stderr

0 commit comments

Comments
 (0)