Spaces:
Running on Zero
Running on Zero
File size: 14,763 Bytes
6a0b176 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | # typing, Hugging Face, configuration, and ML context dependencies
from __future__ import annotations
import json
import re
from typing import Any
from huggingface_hub import InferenceClient
from config import DEFAULT_MAX_TOKENS, HF_MODEL_ID, HF_PROVIDER, HF_TOKEN, MAX_AGENT_STEPS
from ml_engine import MLContext
# Define the system instructions that constrain the ML agent's analysis and final response format
SYSTEM_PROMPT = '''You are a senior Machine Learning Engineering Agent.
Use tools before making claims about the uploaded dataset or model performance. Frame the supervised-learning problem, inspect target behavior, recommend appropriate algorithms, train or compare candidates when useful, and produce reproducible scikit-learn pipeline code grounded in the current dataset.
Do not invent metrics. Only cite metrics returned by tools. Distinguish model evaluation from business impact. Flag leakage, class imbalance, target quality, high-cardinality categoricals, tiny samples, or metric-selection risks when relevant.
Final answer must contain exactly these sections:
## ML assessment
## Dataset and target findings
## Model strategy
## Evaluation
## Risks and next steps
## Recommended Pipeline
```python
...
```
'''
# declare the function-tool schemas exposed to the Hugging Face chat-completion agent
TOOL_SCHEMAS = [
# register the dataset-inspection tool used to summarize dataset structure and sample characteristics
{
"type": "function",
"function": {
"name": "inspect_dataset",
"description": "Inspect dataset rows, columns, types, nulls, cardinality, and examples.",
"parameters": {"type": "object", "properties": {}},
},
},
# register the modeling-setup tool used to infer the task type and suitable candidate algorithms
{
"type": "function",
"function": {
"name": "recommend_modeling_setup",
"description": "Infer or validate the ML problem type for the selected target and recommend candidate algorithms.",
"parameters": {"type": "object", "properties": {}},
},
},
# register the candidate-training tool used to fit and evaluate the selected algorithm
{
"type": "function",
"function": {
"name": "train_candidate_model",
"description": "Train and evaluate the currently selected candidate algorithm on a holdout split.",
"parameters": {"type": "object", "properties": {}},
},
},
# register the comparison tool used to benchmark a compact set of baseline algorithms
{
"type": "function",
"function": {
"name": "compare_algorithms",
"description": "Run a compact deterministic comparison of suitable baseline algorithms.",
"parameters": {"type": "object", "properties": {}},
},
},
# pipeline-generation tool used to produce reproducible scikit-learn code
{
"type": "function",
"function": {
"name": "generate_pipeline_code",
"description": "Generate reproducible scikit-learn pipeline code for the selected target and algorithm.",
"parameters": {"type": "object", "properties": {}},
},
},
]
# Extract the first fenced Python code block from a model response, returning an empty string when absent
def _extract_code(text: str, language: str = "python") -> str:
# Accept common Python fence labels when searching the response text
aliases = [language, "py", "python"]
match = re.search(rf"```(?:{'|'.join(re.escape(x) for x in aliases)})\s*(.*?)```", text or "", re.I | re.S)
return match.group(1).strip() if match else ""
# Normalize a model tool-call object into the dictionary structure expected by the chat message history
def _serialize_tool_call(call: Any) -> dict[str, Any]:
# Read the raw tool arguments so string-encoded JSON can be normalized before serialization
args = call.function.arguments
# Parse JSON argument strings into dictionaries while safely falling back on malformed input
if isinstance(args, str):
try:
args = json.loads(args)
except json.JSONDecodeError:
args = {}
# Return a stable function-call payload with an ID, type, name, and JSON-encoded arguments.
return {
"id": getattr(call, "id", None) or f"call_{call.function.name}",
"type": "function",
"function": {"name": call.function.name, "arguments": json.dumps(args or {})},
}
# Coordinate ML-context tools with an optional Hugging Face model to produce a grounded agent response
class MachineLearningAgent:
# Initialize the agent with dataset context, modeling choices, evaluation settings, and generation controls
def __init__(
self,
context: MLContext,
target: str,
algorithm: str = "Auto",
problem_type: str = "Auto",
test_size: float = 0.2,
temperature: float = 0.15,
max_tokens: int = DEFAULT_MAX_TOKENS,
):
# Store normalized runtime settings and initialize the trace used to record tool execution history
self.context = context
self.target = target
self.algorithm = algorithm or "Auto"
self.problem_type = problem_type or "Auto"
self.test_size = float(test_size)
self.temperature = float(temperature)
self.max_tokens = int(max_tokens)
self.trace: list[dict[str, Any]] = []
# Dispatch a named agent tool to the matching MLContext operation and record its result
def _tool(self, name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]:
# Normalize missing tool arguments to an empty dictionary before dispatch.
arguments = arguments or {}
# Route each supported tool name to the corresponding deterministic ML operation
if name == "inspect_dataset":
result = self.context.compact_profile()
elif name == "recommend_modeling_setup":
result = self.context.modeling_recommendation(self.target, self.problem_type)
elif name == "train_candidate_model":
result = self.context.train_candidate(
self.target,
self.algorithm,
self.problem_type,
self.test_size,
)
elif name == "compare_algorithms":
result = self.context.compare_algorithms(self.target, self.problem_type, self.test_size)
elif name == "generate_pipeline_code":
# Generate reproducible pipeline code and package it with the selected target and algorithm metadata
code = self.context.generate_pipeline_code(self.target, self.algorithm, self.problem_type, self.test_size)
result = {"target": self.target, "algorithm": self.algorithm, "pipeline_code": code}
# Return a structured error payload when the model requests an unsupported tool name
else:
result = {"error": f"Unknown tool: {name}"}
# Append every tool invocation and result to the trace for downstream inspection and reproducibility
self.trace.append({"tool": name, "arguments": arguments, "result": result})
return result
# Build a deterministic response from local ML tooling when model synthesis is unavailable or fails
def _fallback(self, task: str, error: str | None = None):
# Collect dataset profiling, modeling recommendations, training results, and pipeline code for the fallback
profile = self._tool("inspect_dataset")
setup = self._tool("recommend_modeling_setup")
train = self._tool("train_candidate_model")
code = self.context.generate_pipeline_code(self.target, self.algorithm, self.problem_type, self.test_size)
self.trace.append({"tool": "generate_pipeline_code", "arguments": {}, "result": {"pipeline_code": code}})
# Explain whether fallback mode was triggered by a missing token or by a failed model request
if not HF_TOKEN:
note = "HF model synthesis is disabled because `HF_TOKEN` is not configured, so deterministic ML tooling produced this response."
else:
note = f"The HF model request failed, so deterministic ML tooling produced this response. Error: `{error}`"
# Format returned training metrics and assemble the required structured fallback answer
metrics = "\n".join(f"- **{k}**: `{v}`" for k, v in train.get("metrics", {}).items()) or "- No metrics were returned."
answer = f'''## ML assessment
{note}
Task: {task}
Loaded `{profile['source']}` with **{profile['rows']:,} rows** and **{profile['columns']:,} columns**. The selected target is `{self.target}` and the resolved problem type is **{setup['problem_type']}**.
## Dataset and target findings
- Target unique values: **{setup.get('target_unique', 'n/a')}**
- Target nulls: **{setup.get('target_nulls', 'n/a')}**
- Candidate algorithms: {', '.join(setup.get('recommended_algorithms', []))}
## Model strategy
Trained **{train.get('algorithm', self.algorithm)}** using a reusable preprocessing + estimator pipeline with train/test separation.
## Evaluation
{metrics}
## Risks and next steps
- Check for target leakage and time-dependent leakage before production use.
- Match the primary metric to the business cost of false positives, false negatives, or prediction error.
- Add cross-validation and hyperparameter optimization after establishing the baseline.
- Validate drift, calibration, fairness, and operational latency where relevant.
## Recommended Pipeline
```python
{code}
```
'''
# Return the synthesized answer, reusable pipeline code, and complete execution trace together
return answer, code, self.trace
# Execute the agent workflow, using deterministic fallback immediately when no Hugging Face token is configured
def run(self, task: str):
if not HF_TOKEN:
return self._fallback(task)
# HF inference client and seed the conversation with system and task context
client = InferenceClient(token=HF_TOKEN, provider=HF_PROVIDER, timeout=120)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{
"role": "user",
"content": (
f"Task: {task}\n"
f"Selected target: {self.target}\n"
f"Problem type setting: {self.problem_type}\n"
f"Selected algorithm: {self.algorithm}\n"
f"Test size: {self.test_size}\n"
"Use tools first."
),
},
]
# Run bounded model/tool interaction steps and fall back safely if any exception escapes the workflow
try:
# Track the final natural-language response once the model stops requesting tools
final = ""
# Limit iterative model/tool exchanges to the configured maximum number of agent steps
for _ in range(MAX_AGENT_STEPS):
# Request the next assistant message with tool definitions and the current conversation state
response = client.chat_completion(
model=HF_MODEL_ID,
messages=messages,
tools=TOOL_SCHEMAS,
tool_choice="auto",
temperature=self.temperature,
max_tokens=self.max_tokens,
)
# Inspect the returned assistant message for function calls or a completed final response
message = response.choices[0].message
calls = getattr(message, "tool_calls", None) or []
# Stop the loop when the model returns content without requesting any additional tools
if not calls:
final = (message.content or "").strip()
break
# Preserve the assistant's tool-call message in history before executing requested functions
messages.append(
{
"role": "assistant",
"content": message.content or "",
"tool_calls": [_serialize_tool_call(call) for call in calls],
}
)
# Execute each requested tool, normalize its arguments, and append its result as a tool message
for call in calls:
args = call.function.arguments
# Parse string-encoded tool arguments while tolerating malformed JSON from the model
if isinstance(args, str):
try:
args = json.loads(args)
except json.JSONDecodeError:
args = {}
# Dispatch the tool through the local ML context using the normalized argument dictionary
result = self._tool(call.function.name, args or {})
# Add the serialized tool output to the conversation so the model can use it on the next step
messages.append(
{
"role": "tool",
"tool_call_id": getattr(call, "id", None) or f"call_{call.function.name}",
"name": call.function.name,
"content": json.dumps(result, default=str)[:30000],
}
)
# Force a final synthesis request if the bounded loop ends without producing final content
if not final:
response = client.chat_completion(
model=HF_MODEL_ID,
messages=messages + [{"role": "user", "content": "Synthesize the final response now. Do not call more tools."}],
temperature=self.temperature,
max_tokens=self.max_tokens,
)
final = (response.choices[0].message.content or "").strip()
# Reuse Python code from the final response when present, otherwise generate pipeline code deterministically
code = _extract_code(final) or self.context.generate_pipeline_code(
self.target, self.algorithm, self.problem_type, self.test_size
)
# Return the final response, extracted or generated pipeline code, and accumulated tool trace
return final, code, self.trace
# Recover from inference or orchestration errors by returning the deterministic fallback response
except Exception as exc:
return self._fallback(task, str(exc))
|