treelace 0.2.0__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.
Potentially problematic release.
This version of treelace might be problematic. Click here for more details.
- treelace/__init__.py +21 -0
- treelace/app.py +372 -0
- treelace/cli.py +76 -0
- treelace/coding_tool.py +24 -0
- treelace/compiler/__init__.py +16 -0
- treelace/compiler/compile.py +198 -0
- treelace/compiler/compiled.py +43 -0
- treelace/compiler/diagnostics.py +23 -0
- treelace/compiler/inspect.py +133 -0
- treelace/errors.py +46 -0
- treelace/ir/__init__.py +39 -0
- treelace/ir/eval.py +99 -0
- treelace/ir/expressions.py +55 -0
- treelace/ir/nodes.py +115 -0
- treelace/json_types.py +10 -0
- treelace/models/__init__.py +36 -0
- treelace/models/fake.py +37 -0
- treelace/models/openai_adapter.py +83 -0
- treelace/models/protocol.py +52 -0
- treelace/planner/__init__.py +26 -0
- treelace/planner/inspect.py +64 -0
- treelace/planner/models.py +128 -0
- treelace/planner/orchestrator.py +336 -0
- treelace/planner/prompts.py +77 -0
- treelace/py.typed +1 -0
- treelace/runner.py +231 -0
- treelace/runtime/__init__.py +7 -0
- treelace/runtime/engine.py +528 -0
- treelace/runtime/events.py +79 -0
- treelace/runtime/result.py +34 -0
- treelace/sandbox.py +562 -0
- treelace/sessions/__init__.py +22 -0
- treelace/sessions/artifacts.py +54 -0
- treelace/sessions/memory.py +47 -0
- treelace/sessions/models.py +92 -0
- treelace/sessions/protocols.py +32 -0
- treelace/tools/__init__.py +20 -0
- treelace/tools/context.py +34 -0
- treelace/tools/definition.py +53 -0
- treelace/tools/inference.py +167 -0
- treelace/tools/registry.py +88 -0
- treelace/tools/result.py +33 -0
- treelace/tools/spec.py +14 -0
- treelace-0.2.0.dist-info/METADATA +201 -0
- treelace-0.2.0.dist-info/RECORD +48 -0
- treelace-0.2.0.dist-info/WHEEL +4 -0
- treelace-0.2.0.dist-info/entry_points.txt +2 -0
- treelace-0.2.0.dist-info/licenses/LICENSE +177 -0
treelace/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Treelace public API for simple LLM applications."""
|
|
2
|
+
|
|
3
|
+
from treelace.app import NeedsInput, Run, Treelace
|
|
4
|
+
from treelace.planner.inspect import execution_plan_text
|
|
5
|
+
from treelace.planner.prompts import ConversationTurn
|
|
6
|
+
from treelace.tools.context import ToolContext
|
|
7
|
+
from treelace.tools.inference import tool
|
|
8
|
+
from treelace.tools.result import ClarificationRequest, ToolOutput, ToolResult
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ClarificationRequest",
|
|
12
|
+
"ConversationTurn",
|
|
13
|
+
"NeedsInput",
|
|
14
|
+
"Run",
|
|
15
|
+
"ToolContext",
|
|
16
|
+
"ToolOutput",
|
|
17
|
+
"ToolResult",
|
|
18
|
+
"Treelace",
|
|
19
|
+
"execution_plan_text",
|
|
20
|
+
"tool",
|
|
21
|
+
]
|
treelace/app.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""High-level Treelace application facade."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, Coroutine, Iterable, Literal, TypeVar
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
11
|
+
|
|
12
|
+
from treelace.json_types import JsonValue
|
|
13
|
+
from treelace.models import OpenAIChatModel
|
|
14
|
+
from treelace.models.protocol import Message, Model, ModelContext, ModelRequest, StructuredOutput
|
|
15
|
+
from treelace.planner.inspect import execution_plan_text
|
|
16
|
+
from treelace.planner.models import ChatPlan, PlanDecision
|
|
17
|
+
from treelace.planner.prompts import ConversationTurn, MasterPromptManager
|
|
18
|
+
from treelace.runner import Runner
|
|
19
|
+
from treelace.runtime.engine import Engine
|
|
20
|
+
from treelace.runtime.events import Event
|
|
21
|
+
from treelace.runtime.result import RunResult
|
|
22
|
+
from treelace.tools.definition import ToolDefinition
|
|
23
|
+
from treelace.tools.registry import ToolRegistry
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NeedsInput(BaseModel):
|
|
27
|
+
"""Public waiting-state payload used by applications."""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
30
|
+
prompt: str
|
|
31
|
+
options: list[str] = Field(default_factory=list)
|
|
32
|
+
missing_fields: list[str] = Field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Run(BaseModel):
|
|
36
|
+
"""Public result envelope returned by Treelace runs."""
|
|
37
|
+
|
|
38
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
39
|
+
status: Literal["succeeded", "failed", "waiting"]
|
|
40
|
+
question: str
|
|
41
|
+
answer: str | None = None
|
|
42
|
+
data: JsonValue | None = None
|
|
43
|
+
error: str | None = None
|
|
44
|
+
needs_input: NeedsInput | None = None
|
|
45
|
+
plan: PlanDecision | None = None
|
|
46
|
+
plan_text: str | None = None
|
|
47
|
+
events: list[Event] = Field(default_factory=list)
|
|
48
|
+
session_id: str = Field(repr=False)
|
|
49
|
+
clarification_token: str | None = Field(default=None, repr=False)
|
|
50
|
+
raw_output: JsonValue | None = Field(default=None, repr=False)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def prompt(self) -> str | None:
|
|
54
|
+
return self.needs_input.prompt if self.needs_input is not None else None
|
|
55
|
+
|
|
56
|
+
@model_validator(mode="after")
|
|
57
|
+
def _validate_state(self) -> "Run":
|
|
58
|
+
if self.status == "waiting":
|
|
59
|
+
if self.needs_input is None:
|
|
60
|
+
raise ValueError("waiting run requires needs_input")
|
|
61
|
+
if not self.clarification_token:
|
|
62
|
+
raise ValueError("waiting run requires clarification token")
|
|
63
|
+
if self.status == "failed" and not self.error:
|
|
64
|
+
raise ValueError("failed run requires error")
|
|
65
|
+
if self.status == "succeeded" and self.error is not None:
|
|
66
|
+
raise ValueError("succeeded run cannot include error")
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _resolve_openai_model(spec: str) -> Model:
|
|
71
|
+
provider, separator, model_name = spec.partition(":")
|
|
72
|
+
if not separator:
|
|
73
|
+
provider = "openai"
|
|
74
|
+
model_name = spec
|
|
75
|
+
provider = provider.strip().lower()
|
|
76
|
+
if provider not in {"openai", "azure-openai", "azure"}:
|
|
77
|
+
raise ValueError(f"unsupported model provider: {provider}")
|
|
78
|
+
|
|
79
|
+
model = model_name.strip() or (
|
|
80
|
+
os.getenv("OPENAI_MODEL")
|
|
81
|
+
or os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT")
|
|
82
|
+
or os.getenv("AZURE_OPENAI_MODEL_NAME")
|
|
83
|
+
)
|
|
84
|
+
if not model:
|
|
85
|
+
raise ValueError("missing model name (for example: 'openai:gpt-5-mini')")
|
|
86
|
+
|
|
87
|
+
api_key = os.getenv("OPENAI_API_KEY") or os.getenv("AZURE_OPENAI_API_KEY")
|
|
88
|
+
if not api_key:
|
|
89
|
+
raise ValueError("missing OPENAI_API_KEY or AZURE_OPENAI_API_KEY")
|
|
90
|
+
|
|
91
|
+
base_url = os.getenv("OPENAI_BASE_URL")
|
|
92
|
+
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT")
|
|
93
|
+
if not base_url and endpoint:
|
|
94
|
+
base_url = endpoint.rstrip("/") + "/openai/v1/"
|
|
95
|
+
|
|
96
|
+
return OpenAIChatModel(model=model, api_key=api_key, base_url=base_url)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _resolve_model(model: Model | str) -> Model:
|
|
100
|
+
if isinstance(model, str):
|
|
101
|
+
return _resolve_openai_model(model)
|
|
102
|
+
return model
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _extract_data(output: JsonValue | None) -> JsonValue | None:
|
|
106
|
+
if not isinstance(output, dict):
|
|
107
|
+
return output
|
|
108
|
+
model_context = output.get("model_context")
|
|
109
|
+
if model_context is not None:
|
|
110
|
+
return model_context
|
|
111
|
+
trimmed = {
|
|
112
|
+
key: value
|
|
113
|
+
for key, value in output.items()
|
|
114
|
+
if key not in {"artifact_refs", "clarification_request"}
|
|
115
|
+
}
|
|
116
|
+
return trimmed or None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _extract_artifact_refs(output: JsonValue | None) -> list[str]:
|
|
120
|
+
if not isinstance(output, dict):
|
|
121
|
+
return []
|
|
122
|
+
refs = output.get("artifact_refs")
|
|
123
|
+
if not isinstance(refs, list):
|
|
124
|
+
return []
|
|
125
|
+
return [item for item in refs if isinstance(item, str)]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
T = TypeVar("T")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Treelace:
|
|
132
|
+
"""Simple facade for running small LLM-driven tool applications."""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
*,
|
|
137
|
+
model: Model | str | None = None,
|
|
138
|
+
tools: ToolRegistry | Iterable[Any] | None = None,
|
|
139
|
+
instructions: str | None = None,
|
|
140
|
+
planner_model: Model | str | None = None,
|
|
141
|
+
responder_model: Model | str | None = None,
|
|
142
|
+
prompt_manager: MasterPromptManager | None = None,
|
|
143
|
+
engine: Engine | None = None,
|
|
144
|
+
) -> None:
|
|
145
|
+
planner_source = planner_model if planner_model is not None else model
|
|
146
|
+
if planner_source is None:
|
|
147
|
+
raise ValueError("model or planner_model is required")
|
|
148
|
+
responder_source = responder_model if responder_model is not None else planner_source
|
|
149
|
+
|
|
150
|
+
if prompt_manager is not None and instructions:
|
|
151
|
+
raise ValueError("provide either prompt_manager or instructions, not both")
|
|
152
|
+
|
|
153
|
+
manager = prompt_manager or MasterPromptManager(master_prompt=instructions)
|
|
154
|
+
self._instructions = instructions.strip() if isinstance(instructions, str) else ""
|
|
155
|
+
self._runner = Runner(
|
|
156
|
+
model=_resolve_model(planner_source),
|
|
157
|
+
tools=tools,
|
|
158
|
+
prompt_manager=manager,
|
|
159
|
+
engine=engine,
|
|
160
|
+
)
|
|
161
|
+
self._responder_model = _resolve_model(responder_source)
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def tools(self) -> ToolRegistry:
|
|
165
|
+
return self._runner.tools
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def model(self) -> Model:
|
|
169
|
+
"""Model used for planning and for tool-side completions."""
|
|
170
|
+
|
|
171
|
+
return self._runner.planner.model
|
|
172
|
+
|
|
173
|
+
def register_tool(self, tool_like: Any) -> ToolDefinition:
|
|
174
|
+
return self._runner.register_tool(tool_like)
|
|
175
|
+
|
|
176
|
+
async def aplan(
|
|
177
|
+
self,
|
|
178
|
+
*,
|
|
179
|
+
question: str,
|
|
180
|
+
history: list[ConversationTurn] | None = None,
|
|
181
|
+
session_id: str | None = None,
|
|
182
|
+
workflow_id: str = "planned_workflow",
|
|
183
|
+
) -> PlanDecision:
|
|
184
|
+
plan, _ = await self._runner.plan(
|
|
185
|
+
question=question,
|
|
186
|
+
history=history,
|
|
187
|
+
session_id=session_id,
|
|
188
|
+
workflow_id=workflow_id,
|
|
189
|
+
)
|
|
190
|
+
return plan
|
|
191
|
+
|
|
192
|
+
def plan(
|
|
193
|
+
self,
|
|
194
|
+
*,
|
|
195
|
+
question: str,
|
|
196
|
+
history: list[ConversationTurn] | None = None,
|
|
197
|
+
session_id: str | None = None,
|
|
198
|
+
workflow_id: str = "planned_workflow",
|
|
199
|
+
) -> PlanDecision:
|
|
200
|
+
return _run_sync(
|
|
201
|
+
self.aplan(
|
|
202
|
+
question=question,
|
|
203
|
+
history=history,
|
|
204
|
+
session_id=session_id,
|
|
205
|
+
workflow_id=workflow_id,
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
async def arun(
|
|
210
|
+
self,
|
|
211
|
+
question: str,
|
|
212
|
+
*,
|
|
213
|
+
history: list[ConversationTurn] | None = None,
|
|
214
|
+
inputs: JsonValue | None = None,
|
|
215
|
+
respond: bool = True,
|
|
216
|
+
session_id: str | None = None,
|
|
217
|
+
workflow_id: str = "planned_workflow",
|
|
218
|
+
) -> Run:
|
|
219
|
+
result = await self._runner.run(
|
|
220
|
+
question=question,
|
|
221
|
+
inputs={} if inputs is None else inputs,
|
|
222
|
+
history=history,
|
|
223
|
+
session_id=session_id,
|
|
224
|
+
workflow_id=workflow_id,
|
|
225
|
+
)
|
|
226
|
+
return await self._to_public_run(question=question, result=result, respond=respond)
|
|
227
|
+
|
|
228
|
+
def run(
|
|
229
|
+
self,
|
|
230
|
+
question: str,
|
|
231
|
+
*,
|
|
232
|
+
history: list[ConversationTurn] | None = None,
|
|
233
|
+
inputs: JsonValue | None = None,
|
|
234
|
+
respond: bool = True,
|
|
235
|
+
session_id: str | None = None,
|
|
236
|
+
workflow_id: str = "planned_workflow",
|
|
237
|
+
) -> Run:
|
|
238
|
+
return _run_sync(
|
|
239
|
+
self.arun(
|
|
240
|
+
question,
|
|
241
|
+
history=history,
|
|
242
|
+
inputs=inputs,
|
|
243
|
+
respond=respond,
|
|
244
|
+
session_id=session_id,
|
|
245
|
+
workflow_id=workflow_id,
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
run_sync = run
|
|
250
|
+
|
|
251
|
+
async def aresume(self, run: Run, answer: str, *, respond: bool = True) -> Run:
|
|
252
|
+
if run.status != "waiting":
|
|
253
|
+
raise ValueError("resume requires a waiting run")
|
|
254
|
+
if run.clarification_token is None:
|
|
255
|
+
raise ValueError("run does not have a clarification token")
|
|
256
|
+
resumed = await self._runner.resume(
|
|
257
|
+
session_id=run.session_id,
|
|
258
|
+
token=run.clarification_token,
|
|
259
|
+
decision=answer,
|
|
260
|
+
)
|
|
261
|
+
return await self._to_public_run(question=run.question, result=resumed, respond=respond)
|
|
262
|
+
|
|
263
|
+
def resume(self, run: Run, answer: str, *, respond: bool = True) -> Run:
|
|
264
|
+
return _run_sync(self.aresume(run, answer, respond=respond))
|
|
265
|
+
|
|
266
|
+
resume_sync = resume
|
|
267
|
+
|
|
268
|
+
async def _to_public_run(
|
|
269
|
+
self,
|
|
270
|
+
*,
|
|
271
|
+
question: str,
|
|
272
|
+
result: RunResult,
|
|
273
|
+
respond: bool,
|
|
274
|
+
) -> Run:
|
|
275
|
+
plan_text = execution_plan_text(result.plan) if result.plan is not None else None
|
|
276
|
+
pending = result.pending_clarification
|
|
277
|
+
needs_input = (
|
|
278
|
+
NeedsInput(
|
|
279
|
+
prompt=pending.prompt,
|
|
280
|
+
options=list(pending.options),
|
|
281
|
+
missing_fields=list(pending.missing_fields),
|
|
282
|
+
)
|
|
283
|
+
if pending is not None
|
|
284
|
+
else None
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
data = _extract_data(result.output)
|
|
288
|
+
answer: str | None = None
|
|
289
|
+
if isinstance(result.plan, ChatPlan):
|
|
290
|
+
answer = result.plan.response
|
|
291
|
+
data = {"message": result.plan.response}
|
|
292
|
+
elif result.status == "succeeded" and respond:
|
|
293
|
+
answer = await self._synthesize_answer(
|
|
294
|
+
question=question,
|
|
295
|
+
plan_text=plan_text or "(no plan)",
|
|
296
|
+
data=data,
|
|
297
|
+
artifact_refs=_extract_artifact_refs(result.output),
|
|
298
|
+
session_id=result.session_id,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return Run(
|
|
302
|
+
status=result.status,
|
|
303
|
+
question=question,
|
|
304
|
+
answer=answer,
|
|
305
|
+
data=data,
|
|
306
|
+
error=result.error,
|
|
307
|
+
needs_input=needs_input,
|
|
308
|
+
plan=result.plan,
|
|
309
|
+
plan_text=plan_text,
|
|
310
|
+
events=list(result.events),
|
|
311
|
+
session_id=result.session_id,
|
|
312
|
+
clarification_token=pending.token if pending is not None else None,
|
|
313
|
+
raw_output=result.output,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
async def _synthesize_answer(
|
|
317
|
+
self,
|
|
318
|
+
*,
|
|
319
|
+
question: str,
|
|
320
|
+
plan_text: str,
|
|
321
|
+
data: JsonValue | None,
|
|
322
|
+
artifact_refs: list[str],
|
|
323
|
+
session_id: str,
|
|
324
|
+
) -> str:
|
|
325
|
+
if data is None:
|
|
326
|
+
return "No result data was produced."
|
|
327
|
+
|
|
328
|
+
system_lines = [
|
|
329
|
+
"You are Treelace's response synthesizer.",
|
|
330
|
+
"Answer strictly from the provided execution data.",
|
|
331
|
+
"Do not invent tools, values, or files.",
|
|
332
|
+
"If data is missing for part of the question, say so clearly.",
|
|
333
|
+
"Keep the answer concise unless the question asks for detail.",
|
|
334
|
+
]
|
|
335
|
+
if self._instructions:
|
|
336
|
+
system_lines.append(f"Application instructions:\n{self._instructions}")
|
|
337
|
+
|
|
338
|
+
prompt_payload = {
|
|
339
|
+
"question": question,
|
|
340
|
+
"plan": plan_text,
|
|
341
|
+
"data": data,
|
|
342
|
+
"artifact_refs": artifact_refs,
|
|
343
|
+
}
|
|
344
|
+
response = await self._responder_model.complete(
|
|
345
|
+
ModelRequest(
|
|
346
|
+
messages=[
|
|
347
|
+
Message(role="system", content="\n".join(system_lines)),
|
|
348
|
+
Message(
|
|
349
|
+
role="user",
|
|
350
|
+
content=json.dumps(prompt_payload, indent=2, ensure_ascii=True, sort_keys=True),
|
|
351
|
+
),
|
|
352
|
+
]
|
|
353
|
+
),
|
|
354
|
+
context=ModelContext(session_id=session_id, metadata={"stage": "response_synthesis"}),
|
|
355
|
+
)
|
|
356
|
+
if isinstance(response.output, StructuredOutput):
|
|
357
|
+
answer = response.output.data.get("answer")
|
|
358
|
+
if isinstance(answer, str) and answer.strip():
|
|
359
|
+
return answer.strip()
|
|
360
|
+
return json.dumps(response.output.data, ensure_ascii=True, sort_keys=True)
|
|
361
|
+
text = response.output.text.strip()
|
|
362
|
+
if text:
|
|
363
|
+
return text
|
|
364
|
+
return json.dumps(data, ensure_ascii=True, sort_keys=True)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _run_sync(coro: Coroutine[Any, Any, T]) -> T:
|
|
368
|
+
try:
|
|
369
|
+
asyncio.get_running_loop()
|
|
370
|
+
except RuntimeError:
|
|
371
|
+
return asyncio.run(coro)
|
|
372
|
+
raise RuntimeError("synchronous Treelace methods cannot run inside an active event loop")
|
treelace/cli.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Treelace CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from treelace.compiler.compile import compile_workflow
|
|
10
|
+
from treelace.compiler.inspect import dependency_mermaid, dependency_text
|
|
11
|
+
from treelace.ir.nodes import Workflow
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _load_workflow(path: str) -> Workflow:
|
|
15
|
+
payload = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
16
|
+
return Workflow.model_validate(payload)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(argv: list[str] | None = None) -> int:
|
|
20
|
+
parser = argparse.ArgumentParser(prog="treelace")
|
|
21
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
22
|
+
|
|
23
|
+
validate_cmd = subparsers.add_parser("validate")
|
|
24
|
+
validate_cmd.add_argument("workflow")
|
|
25
|
+
|
|
26
|
+
inspect_cmd = subparsers.add_parser("inspect")
|
|
27
|
+
inspect_cmd.add_argument("workflow")
|
|
28
|
+
|
|
29
|
+
render_cmd = subparsers.add_parser("render")
|
|
30
|
+
render_cmd.add_argument("workflow")
|
|
31
|
+
render_cmd.add_argument("-o", "--output", required=True)
|
|
32
|
+
|
|
33
|
+
schema_cmd = subparsers.add_parser("schema")
|
|
34
|
+
schema_cmd.add_argument(
|
|
35
|
+
"--target",
|
|
36
|
+
choices=["workflow", "plan_decision"],
|
|
37
|
+
default="workflow",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
args = parser.parse_args(argv)
|
|
41
|
+
|
|
42
|
+
if args.command == "validate":
|
|
43
|
+
workflow = _load_workflow(args.workflow)
|
|
44
|
+
compile_workflow(workflow)
|
|
45
|
+
print("ok")
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
if args.command == "inspect":
|
|
49
|
+
workflow = _load_workflow(args.workflow)
|
|
50
|
+
compiled = compile_workflow(workflow)
|
|
51
|
+
print(dependency_text(compiled))
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
if args.command == "render":
|
|
55
|
+
workflow = _load_workflow(args.workflow)
|
|
56
|
+
compiled = compile_workflow(workflow)
|
|
57
|
+
mermaid = dependency_mermaid(compiled)
|
|
58
|
+
Path(args.output).write_text(mermaid, encoding="utf-8")
|
|
59
|
+
print(args.output)
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
if args.command == "schema":
|
|
63
|
+
if args.target == "workflow":
|
|
64
|
+
schema = Workflow.model_json_schema()
|
|
65
|
+
else:
|
|
66
|
+
from treelace.planner.models import PLAN_DECISION_ADAPTER
|
|
67
|
+
|
|
68
|
+
schema = PLAN_DECISION_ADAPTER.json_schema()
|
|
69
|
+
print(json.dumps(schema, indent=2))
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
raise SystemExit(main())
|
treelace/coding_tool.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Small local coding utility for dataframe-like payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def coding_tool(
|
|
9
|
+
dataframes: list[Any],
|
|
10
|
+
dataframe_schemas: list[dict[str, Any]],
|
|
11
|
+
system_message: str = "",
|
|
12
|
+
) -> tuple[dict[str, Any], list[Any]]:
|
|
13
|
+
"""Return compact metadata and pass-through artifacts.
|
|
14
|
+
|
|
15
|
+
This intentionally avoids model calls and is safe for local use as a helper
|
|
16
|
+
inside registered tools.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
_ = dataframe_schemas
|
|
20
|
+
summary = {
|
|
21
|
+
"dataframe_count": len(dataframes),
|
|
22
|
+
"system_message_preview": system_message[:200],
|
|
23
|
+
}
|
|
24
|
+
return summary, dataframes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Compiler exports."""
|
|
2
|
+
|
|
3
|
+
from treelace.compiler.compile import compile_workflow
|
|
4
|
+
from treelace.compiler.compiled import CompiledOperation, CompiledWorkflow
|
|
5
|
+
from treelace.compiler.diagnostics import Diagnostic
|
|
6
|
+
from treelace.compiler.inspect import dependency_json, dependency_mermaid, dependency_text
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"CompiledOperation",
|
|
10
|
+
"CompiledWorkflow",
|
|
11
|
+
"Diagnostic",
|
|
12
|
+
"compile_workflow",
|
|
13
|
+
"dependency_json",
|
|
14
|
+
"dependency_mermaid",
|
|
15
|
+
"dependency_text",
|
|
16
|
+
]
|