项目文件夹

文件
wehub-resource-sync 327604cc89
Rebuild Cookbook Website / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:41:49 +08:00

226 行
8.7 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
from __future__ import annotations
import json, time, uuid, logging, re
from dataclasses import dataclass, asdict, field
from pathlib import Path
from typing import Any, Dict, List
from openai import OpenAI
# --- tool backends -------------------------
from tools import chem_lookup, cost_estimator, outcome_db, literature_search, list_available_chemicals
# ---------- tiny infrastructure helpers --------------------------------------
# Holds run-specific parameters provided by user.
@dataclass
class Context:
compound: str
goal: str
budget: float
time_h: int
previous: str
client: OpenAI
run_id: str = field(default_factory=lambda: uuid.uuid4().hex[:8])
def prompt_vars(self):
return {
"compound": self.compound,
"goal": self.goal,
"budget": self.budget,
"time_h": self.time_h,
"previous": self.previous,
}
# -- Functioncalling tool manifest --------------------
def load_tools():
return [
{
"type": "function",
"function": {
"name": "chem_lookup",
"description": "Mock function to look up chemical properties.",
"parameters": {
"type": "object",
"properties": {
"chemical_name": {
"type": "string",
"description": "The name of the chemical to look up."
},
"property": {
"type": "string",
"description": "Optional specific property to retrieve (e.g., 'melting_point'). If None, returns all properties."
}
},
"required": ["chemical_name"]
}
}
},
{
"type": "function",
"function": {
"name": "cost_estimator",
"description": "Mock function to estimate the cost of reagents and procedures.",
"parameters": {
"type": "object",
"properties": {
"reagents": {
"type": "array",
"description": "List of reagents, where each reagent is a dictionary with 'name', 'amount', and 'unit'.",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Name of the reagent."},
"amount": {"type": "number", "description": "Amount of the reagent."},
"unit": {"type": "string", "description": "Unit for the amount (e.g., 'g', 'mg', 'kg')."}
},
"required": ["name", "amount", "unit"]
}
},
"equipment": {
"type": "array",
"description": "Optional list of equipment items used.",
"items": {"type": "string"}
},
"duration_hours": {
"type": "number",
"description": "Optional duration of the procedure in hours for labor cost calculation."
}
},
}
}
},
{
"type": "function",
"function": {
"name": "outcome_db",
"description": "Mock function to query the database of past experiment outcomes.",
"parameters": {
"type": "object",
"properties": {
"compound": {
"type": "string",
"description": "The chemical compound name to query past experiments for."
},
"parameter": {
"type": "string",
"description": "Optional specific parameter to filter experiments by (e.g., 'yield', 'temperature')."
},
"limit": {
"type": "integer",
"description": "Maximum number of experiment results to return (default: 5)."
}
},
"required": ["compound"]
}
}
},
{
"type": "function",
"function": {
"name": "literature_search",
"description": "Mock function to search scientific literature for relevant information.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query (keywords) for the literature search."
},
"filter": {
"type": "string",
"description": "Optional filter string, potentially including year (e.g., '2023') or journal name."
},
"limit": {
"type": "integer",
"description": "Maximum number of search results to return (default: 3)."
}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "list_available_chemicals",
"description": "Provides a list of all chemical names available in the database.",
"parameters": {
"type": "object",
"properties": {},
# No parameters needed for this tool
}
}
}
]
# -- minimal logger -----------------------------------------------------------
def log_json(stage: str, data: Any, ctx: Context):
Path("logs").mkdir(exist_ok=True)
p = Path("logs") / f"{ctx.run_id}.log"
with p.open("a", encoding="utf-8") as f:
f.write(json.dumps({"ts": time.time(), "stage": stage, "data": data}, indent=2) + "\n")
# -- JSON extractor -----------------------------------------------------
def _parse_json(text: str) -> Dict[str, Any]:
try:
return json.loads(text)
except json.JSONDecodeError:
# try to rescue JSON from a ```json ...``` block
m = re.search(r"```(?:json)?\\s*(.*?)```", text, re.S)
if m:
try:
return json.loads(m.group(1))
except json.JSONDecodeError:
pass # fall-through to raw
return {"raw": text} # give caller *something* parsable
# -- tool call handler --------------------------------------------------------
def _dispatch_tool(name: str, args: Dict[str, Any]):
"""Run the local Python implementation of a tool.
If the model supplied bad / missing arguments, return an error JSON instead
of raising – so the conversation can continue."""
try:
return {
"chem_lookup": chem_lookup,
"cost_estimator": cost_estimator,
"outcome_db": outcome_db,
"literature_search": literature_search,
"list_available_chemicals": list_available_chemicals,
}[name](**args)
except TypeError as e:
# log & surface the problem back to the model in a structured way
logging.warning(f"Tool {name} failed: {e}")
return {"tool_error": str(e), "supplied_args": args}
# -- unified OpenAI call w/ recursive tool handling ---------------------------
def call_openai(client: OpenAI, model: str, system: str, user: str, ctx: Context):
messages = [
{"role": "system", "content": system},
{"role": "user", "content": user},
]
while True:
resp = client.chat.completions.create(
model=model,
messages=messages,
tools=load_tools(),
tool_choice="auto",
)
msg = resp.choices[0].message
messages.append(msg.model_dump(exclude_unset=True))
if not msg.tool_calls:
log_json(model, msg.content, ctx)
return _parse_json(msg.content)
# handle first tool call, then loop again
for tc in msg.tool_calls:
result = _dispatch_tool(tc.function.name, json.loads(tc.function.arguments))
messages.append({
"role": "tool", "tool_call_id": tc.id,
"content": json.dumps(result)
})