mirror of
https://github.com/hoshikawa2/openclaw-oci.git
synced 2026-03-06 18:21:00 +00:00
Linux commands OK
This commit is contained in:
@@ -16,6 +16,417 @@ import os
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# CONFIG
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
OCI_CONFIG_FILE = os.getenv("OCI_CONFIG_FILE", os.path.expanduser("~/.oci/config"))
|
||||||
|
OCI_PROFILE = os.getenv("OCI_PROFILE", "DEFAULT")
|
||||||
|
OCI_COMPARTMENT_ID = os.getenv("OCI_COMPARTMENT_ID", "<YOUR_OCI_COMPARTMENT_ID>")
|
||||||
|
OCI_GENAI_ENDPOINT = os.getenv(
|
||||||
|
"OCI_GENAI_ENDPOINT",
|
||||||
|
"https://inference.generativeai.<oci_region>.oci.oraclecloud.com"
|
||||||
|
)
|
||||||
|
if not OCI_COMPARTMENT_ID:
|
||||||
|
raise RuntimeError("OCI_COMPARTMENT_ID not defined")
|
||||||
|
|
||||||
|
OPENCLAW_TOOLS_ACTIVE = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# PROMPTS to adapt for OCI
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
SYSTEM_AGENT_PROMPT = """
|
||||||
|
You are an autonomous software agent.
|
||||||
|
|
||||||
|
You have full access to the local machine.
|
||||||
|
|
||||||
|
Available tools:
|
||||||
|
- weather(city: string)
|
||||||
|
- exec(command: string)
|
||||||
|
|
||||||
|
If a system command is required, respond ONLY with:
|
||||||
|
|
||||||
|
{
|
||||||
|
"action": "call_tool",
|
||||||
|
"tool": "exec",
|
||||||
|
"arguments": {
|
||||||
|
"command": "<shell command>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
***VERY IMPORTANT***: A TASK IS CONSIDERED COMPLETED WHEN IT RESULTS IN A ARTIFACT ASKED FROM THE USER
|
||||||
|
|
||||||
|
If task is completed:
|
||||||
|
|
||||||
|
{
|
||||||
|
"action": "final_answer",
|
||||||
|
"content": "<result>"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
RUNNER_PROMPT = (
|
||||||
|
"You are a Linux execution agent.\n"
|
||||||
|
"\n"
|
||||||
|
"OUTPUT CONTRACT (MANDATORY):\n"
|
||||||
|
"- You must output EXACTLY ONE of the following per response:\n"
|
||||||
|
" A) (exec <command>)\n"
|
||||||
|
" B) (done <final answer>)\n"
|
||||||
|
"\n"
|
||||||
|
"STRICT RULES:\n"
|
||||||
|
"1) NEVER output raw commands without (exec <command>). Raw commands will be ignored.\n"
|
||||||
|
"2) NEVER output explanations, markdown, code fences, bullets, or extra text.\n"
|
||||||
|
"3) If you need to create multi-line files, you MUST use heredoc inside (exec <command>), e.g.:\n"
|
||||||
|
" (exec cat > file.py << 'EOF'\n"
|
||||||
|
" ...\n"
|
||||||
|
" EOF)\n"
|
||||||
|
"4) If the previous tool result shows an error, your NEXT response must be (exec <command>) to fix it.\n"
|
||||||
|
"5) When the artifact is created successfully, end with (done ...).\n"
|
||||||
|
"\n"
|
||||||
|
"REMINDER: Your response must be only a single parenthesized block."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mapeamento OpenAI → OCI
|
||||||
|
MODEL_MAP = {
|
||||||
|
"gpt-5": "openai.gpt-4.1",
|
||||||
|
"openai/gpt-5": "openai.gpt-4.1",
|
||||||
|
"openai-compatible/gpt-5": "openai.gpt-4.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# FASTAPI APP
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
app = FastAPI(title="OCI OpenAI-Compatible Gateway")
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# OCI SIGNER
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def get_signer():
|
||||||
|
config = oci.config.from_file(OCI_CONFIG_FILE, OCI_PROFILE)
|
||||||
|
return oci.signer.Signer(
|
||||||
|
tenancy=config["tenancy"],
|
||||||
|
user=config["user"],
|
||||||
|
fingerprint=config["fingerprint"],
|
||||||
|
private_key_file_location=config["key_file"],
|
||||||
|
pass_phrase=config.get("pass_phrase"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# OCI CHAT CALL (OPENAI FORMAT)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def _openai_messages_to_generic(messages: list) -> list:
|
||||||
|
"""
|
||||||
|
OpenAI: {"role":"user","content":"..."}
|
||||||
|
Generic: {"role":"USER","content":[{"type":"TEXT","text":"..."}]}
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
for m in messages or []:
|
||||||
|
role = (m.get("role") or "user").upper()
|
||||||
|
|
||||||
|
# OCI GENERIC geralmente espera USER/ASSISTANT
|
||||||
|
if role == "SYSTEM":
|
||||||
|
role = "USER"
|
||||||
|
elif role == "TOOL":
|
||||||
|
role = "USER"
|
||||||
|
|
||||||
|
content = m.get("content", "")
|
||||||
|
|
||||||
|
# Se vier lista (OpenAI multimodal), extrai texto
|
||||||
|
if isinstance(content, list):
|
||||||
|
parts = []
|
||||||
|
for item in content:
|
||||||
|
if isinstance(item, dict) and item.get("type") in ("text", "TEXT"):
|
||||||
|
parts.append(item.get("text", ""))
|
||||||
|
content = "\n".join(parts)
|
||||||
|
|
||||||
|
out.append({
|
||||||
|
"role": role,
|
||||||
|
"content": [{"type": "TEXT", "text": str(content)}]
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
def build_generic_messages(openai_messages: list, system_prompt: str) -> list:
|
||||||
|
out = []
|
||||||
|
# 1) Injeta o system como PRIMEIRA mensagem USER, com prefixo fixo
|
||||||
|
out.append({
|
||||||
|
"role": "USER",
|
||||||
|
"content": [{"type":"TEXT","text": "SYSTEM:\n" + system_prompt.strip()}]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2) Depois converte o resto, ignorando systems originais
|
||||||
|
for m in openai_messages or []:
|
||||||
|
role = (m.get("role") or "user").lower()
|
||||||
|
if role == "system":
|
||||||
|
continue
|
||||||
|
|
||||||
|
r = "USER" if role in ("user", "tool") else "ASSISTANT"
|
||||||
|
content = m.get("content", "")
|
||||||
|
|
||||||
|
if isinstance(content, list):
|
||||||
|
parts = []
|
||||||
|
for item in content:
|
||||||
|
if isinstance(item, dict) and item.get("type") in ("text","TEXT"):
|
||||||
|
parts.append(item.get("text",""))
|
||||||
|
content = "\n".join(parts)
|
||||||
|
|
||||||
|
out.append({
|
||||||
|
"role": r,
|
||||||
|
"content": [{"type":"TEXT","text": str(content)}]
|
||||||
|
})
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def call_oci_chat(body: dict, system_prompt: str):
|
||||||
|
signer = get_signer()
|
||||||
|
|
||||||
|
model = body.get("model")
|
||||||
|
oci_model = MODEL_MAP.get(model, model)
|
||||||
|
|
||||||
|
url = f"{OCI_GENAI_ENDPOINT}/20231130/actions/chat"
|
||||||
|
|
||||||
|
# generic_messages = _openai_messages_to_generic(body.get("messages", []))
|
||||||
|
generic_messages = build_generic_messages(body.get("messages", []), system_prompt)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"compartmentId": OCI_COMPARTMENT_ID,
|
||||||
|
"servingMode": {
|
||||||
|
"servingType": "ON_DEMAND",
|
||||||
|
"modelId": oci_model
|
||||||
|
},
|
||||||
|
"chatRequest": {
|
||||||
|
"apiFormat": "GENERIC",
|
||||||
|
"messages": generic_messages,
|
||||||
|
"maxTokens": int(body.get("max_tokens", 4000)),
|
||||||
|
"temperature": float(body.get("temperature", 0.0)),
|
||||||
|
"topP": float(body.get("top_p", 1.0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ⚠️ IMPORTANTÍSSIMO:
|
||||||
|
# Em GENERIC, NÃO envie tools/tool_choice/stream (você orquestra tools no proxy)
|
||||||
|
# Se você mandar, pode dar 400 "correct format of request".
|
||||||
|
|
||||||
|
# print("\n=== PAYLOAD FINAL (GENERIC) ===")
|
||||||
|
# print(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
r = requests.post(url, json=payload, auth=signer)
|
||||||
|
if r.status_code != 200:
|
||||||
|
print("OCI ERROR:", r.text)
|
||||||
|
raise HTTPException(status_code=r.status_code, detail=r.text)
|
||||||
|
|
||||||
|
return r.json()["chatResponse"]
|
||||||
|
|
||||||
|
def detect_tool_call(text: str):
|
||||||
|
pattern = r"exec\s*\(\s*([^\s]+)\s*(.*?)\s*\)"
|
||||||
|
match = re.search(pattern, text)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
|
||||||
|
tool_name = "exec"
|
||||||
|
command = match.group(1)
|
||||||
|
args = match.group(2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"tool": tool_name,
|
||||||
|
"args_raw": f"{command} {args}".strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
def execute_exec_command(command: str):
|
||||||
|
try:
|
||||||
|
print(f"LOG: EXEC COMMAND: {command}")
|
||||||
|
p = subprocess.run(
|
||||||
|
command,
|
||||||
|
shell=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=120 # ajuste
|
||||||
|
)
|
||||||
|
out = (p.stdout or "") + (p.stderr or "")
|
||||||
|
return out if out.strip() else f"(no output) exit={p.returncode}"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return "ERROR: command timed out"
|
||||||
|
|
||||||
|
TOOLS = {
|
||||||
|
"weather": lambda city: get_weather_from_api(city),
|
||||||
|
"exec": lambda command: execute_exec_command(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
def execute_real_tool(name, args):
|
||||||
|
|
||||||
|
if name == "weather":
|
||||||
|
city = args.get("city")
|
||||||
|
return get_weather_from_api(city)
|
||||||
|
|
||||||
|
return "Tool not implemented"
|
||||||
|
|
||||||
|
def _extract_generic_text(oci_message: dict) -> str:
|
||||||
|
content = oci_message.get("content")
|
||||||
|
if isinstance(content, list):
|
||||||
|
r = "".join([i.get("text", "") for i in content if isinstance(i, dict) and i.get("type") == "TEXT"])
|
||||||
|
# print("r", r)
|
||||||
|
return r
|
||||||
|
if isinstance(content, str):
|
||||||
|
# print("content", content)
|
||||||
|
return content
|
||||||
|
return str(content)
|
||||||
|
|
||||||
|
|
||||||
|
def agent_loop(body: dict, max_iterations=10000):
|
||||||
|
|
||||||
|
# Trabalhe sempre com OpenAI messages internamente,
|
||||||
|
# mas call_oci_chat converte pra GENERIC.
|
||||||
|
messages = []
|
||||||
|
messages.append({"role": "system", "content": SYSTEM_AGENT_PROMPT})
|
||||||
|
messages.extend(body.get("messages", []))
|
||||||
|
|
||||||
|
for _ in range(max_iterations):
|
||||||
|
|
||||||
|
response = call_oci_chat({**body, "messages": messages}, SYSTEM_AGENT_PROMPT)
|
||||||
|
|
||||||
|
oci_choice = response["choices"][0]
|
||||||
|
oci_message = oci_choice["message"]
|
||||||
|
|
||||||
|
text = _extract_generic_text(oci_message)
|
||||||
|
|
||||||
|
try:
|
||||||
|
agent_output = json.loads(text)
|
||||||
|
except:
|
||||||
|
# modelo não retornou JSON (quebrou regra)
|
||||||
|
return response
|
||||||
|
|
||||||
|
if agent_output.get("action") == "call_tool":
|
||||||
|
tool_name = agent_output.get("tool")
|
||||||
|
args = agent_output.get("arguments", {})
|
||||||
|
|
||||||
|
if tool_name not in TOOLS:
|
||||||
|
# devolve pro modelo como erro
|
||||||
|
messages.append({"role": "assistant", "content": text})
|
||||||
|
messages.append({"role": "user", "content": json.dumps({
|
||||||
|
"tool_error": f"Tool '{tool_name}' not implemented"
|
||||||
|
})})
|
||||||
|
continue
|
||||||
|
|
||||||
|
tool_result = TOOLS[tool_name](**args)
|
||||||
|
|
||||||
|
# Mantém o histórico: (1) decisão do agente, (2) resultado do tool
|
||||||
|
messages.append({"role": "assistant", "content": text})
|
||||||
|
messages.append({"role": "user", "content": json.dumps({
|
||||||
|
"tool_result": {
|
||||||
|
"tool": tool_name,
|
||||||
|
"arguments": args,
|
||||||
|
"result": tool_result
|
||||||
|
}
|
||||||
|
}, ensure_ascii=False)})
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
if agent_output.get("action") == "final_answer":
|
||||||
|
return response
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
EXEC_RE = re.compile(r"\(exec\s+(.+?)\)\s*$", re.DOTALL)
|
||||||
|
DONE_RE = re.compile(r"\(done\s+(.+?)\)\s*$", re.MULTILINE)
|
||||||
|
|
||||||
|
def run_exec_loop(body: dict, max_steps: int = 10000) -> dict:
|
||||||
|
# Histórico OpenAI-style
|
||||||
|
messages = [{"role":"system"}]
|
||||||
|
messages.extend(body.get("messages", []))
|
||||||
|
|
||||||
|
last = None
|
||||||
|
|
||||||
|
for _ in range(max_steps):
|
||||||
|
last = call_oci_chat({**body, "messages": messages}, RUNNER_PROMPT)
|
||||||
|
print('LLM Result', last)
|
||||||
|
msg = last["choices"][0]["message"]
|
||||||
|
text = _extract_generic_text(msg) or ""
|
||||||
|
|
||||||
|
m_done = DONE_RE.search(text)
|
||||||
|
print("DONE_RE", text)
|
||||||
|
print("m_done", m_done)
|
||||||
|
if m_done:
|
||||||
|
final_text = m_done.group(1).strip()
|
||||||
|
|
||||||
|
# devolve em formato OpenAI no fim
|
||||||
|
return {
|
||||||
|
**last,
|
||||||
|
"choices": [{
|
||||||
|
**last["choices"][0],
|
||||||
|
"message": {"role":"assistant","content": final_text},
|
||||||
|
"finishReason": "stop"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
m_exec = EXEC_RE.search(text)
|
||||||
|
if m_exec:
|
||||||
|
command = m_exec.group(1).strip()
|
||||||
|
result = execute_exec_command(command)
|
||||||
|
|
||||||
|
messages.append({"role":"assistant","content": text})
|
||||||
|
messages.append({"role":"user","content": f"Tool result:\n{result}"})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Se o modelo quebrou o protocolo:
|
||||||
|
messages.append({"role":"assistant","content": text})
|
||||||
|
messages.append({"role":"user","content": (
|
||||||
|
"Protocol error. You MUST reply ONLY with (exec <command>) or (done <final answer>)."
|
||||||
|
)})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# estourou steps: devolve última resposta (melhor do que travar)
|
||||||
|
return last
|
||||||
|
|
||||||
|
def verify_task_completion(original_task: str, assistant_output: str) -> bool:
|
||||||
|
"""
|
||||||
|
Retorna True se tarefa estiver concluída.
|
||||||
|
Retorna False se ainda precisar continuar.
|
||||||
|
"""
|
||||||
|
|
||||||
|
verifier_prompt = [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": (
|
||||||
|
"You are a strict task completion validator.\n"
|
||||||
|
"Answer ONLY with DONE or CONTINUE.\n"
|
||||||
|
"DONE = the task is fully completed.\n"
|
||||||
|
"CONTINUE = more steps are required.\n"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": f"""
|
||||||
|
Original task:
|
||||||
|
{original_task}
|
||||||
|
|
||||||
|
Last assistant output:
|
||||||
|
{assistant_output}
|
||||||
|
|
||||||
|
Is the task fully completed?
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
response = call_oci_chat({
|
||||||
|
"model": "openai-compatible/gpt-5",
|
||||||
|
"messages": verifier_prompt,
|
||||||
|
"temperature": 0
|
||||||
|
}, verifier_prompt[0]["content"])
|
||||||
|
|
||||||
|
text = _extract_generic_text(response["choices"][0]["message"]).strip().upper()
|
||||||
|
|
||||||
|
return text == "DONE"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ENTERPRISE TOOLS
|
||||||
|
# Set the OPENCLAW_TOOLS_ACTIVE = True to automatize OpenClaw execution Tools
|
||||||
|
# Set the OPENCLAW_TOOLS_ACTIVE = False and implement your own Tools
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
def get_weather_from_api(city: str) -> str:
|
def get_weather_from_api(city: str) -> str:
|
||||||
"""
|
"""
|
||||||
Consulta clima atual usando Open-Meteo (100% free, sem API key)
|
Consulta clima atual usando Open-Meteo (100% free, sem API key)
|
||||||
@@ -78,245 +489,6 @@ def get_weather_from_api(city: str) -> str:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Weather tool error: {str(e)}"
|
return f"Weather tool error: {str(e)}"
|
||||||
# ============================================================
|
|
||||||
# CONFIG
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
OCI_CONFIG_FILE = os.getenv("OCI_CONFIG_FILE", os.path.expanduser("~/.oci/config"))
|
|
||||||
OCI_PROFILE = os.getenv("OCI_PROFILE", "DEFAULT")
|
|
||||||
OCI_COMPARTMENT_ID = os.getenv("OCI_COMPARTMENT_ID", "<YOUR_OCI_COMPARTMENT_ID>")
|
|
||||||
OCI_GENAI_ENDPOINT = os.getenv(
|
|
||||||
"OCI_GENAI_ENDPOINT",
|
|
||||||
"https://inference.generativeai.us-chicago-1.oci.oraclecloud.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
OPENCLAW_TOOLS_ACTIVE = True
|
|
||||||
|
|
||||||
SYSTEM_AGENT_PROMPT = """
|
|
||||||
You are an enterprise AI agent.
|
|
||||||
|
|
||||||
You MUST respond ONLY in valid JSON.
|
|
||||||
|
|
||||||
Available tools:
|
|
||||||
- weather(city: string)
|
|
||||||
|
|
||||||
Response format:
|
|
||||||
|
|
||||||
If you need to call a tool:
|
|
||||||
{
|
|
||||||
"action": "call_tool",
|
|
||||||
"tool": "<tool_name>",
|
|
||||||
"arguments": { ... }
|
|
||||||
}
|
|
||||||
|
|
||||||
If you are returning a final answer:
|
|
||||||
{
|
|
||||||
"action": "final_answer",
|
|
||||||
"content": "<final user answer>"
|
|
||||||
}
|
|
||||||
|
|
||||||
Never include explanations outside JSON.
|
|
||||||
"""
|
|
||||||
|
|
||||||
TOOLS = {
|
|
||||||
"weather": lambda city: get_weather_from_api(city)
|
|
||||||
}
|
|
||||||
|
|
||||||
if not OCI_COMPARTMENT_ID:
|
|
||||||
raise RuntimeError("OCI_COMPARTMENT_ID not defined")
|
|
||||||
|
|
||||||
# Mapeamento OpenAI → OCI
|
|
||||||
MODEL_MAP = {
|
|
||||||
"gpt-5": "openai.gpt-4.1",
|
|
||||||
"openai/gpt-5": "openai.gpt-4.1",
|
|
||||||
"openai-compatible/gpt-5": "openai.gpt-4.1",
|
|
||||||
}
|
|
||||||
|
|
||||||
app = FastAPI(title="OCI OpenAI-Compatible Gateway")
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# OCI SIGNER
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def get_signer():
|
|
||||||
config = oci.config.from_file(OCI_CONFIG_FILE, OCI_PROFILE)
|
|
||||||
return oci.signer.Signer(
|
|
||||||
tenancy=config["tenancy"],
|
|
||||||
user=config["user"],
|
|
||||||
fingerprint=config["fingerprint"],
|
|
||||||
private_key_file_location=config["key_file"],
|
|
||||||
pass_phrase=config.get("pass_phrase"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# OCI CHAT CALL (OPENAI FORMAT)
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def _openai_messages_to_generic(messages: list) -> list:
|
|
||||||
"""
|
|
||||||
OpenAI: {"role":"user","content":"..."}
|
|
||||||
Generic: {"role":"USER","content":[{"type":"TEXT","text":"..."}]}
|
|
||||||
"""
|
|
||||||
out = []
|
|
||||||
for m in messages or []:
|
|
||||||
role = (m.get("role") or "user").upper()
|
|
||||||
|
|
||||||
# OCI GENERIC geralmente espera USER/ASSISTANT
|
|
||||||
if role == "SYSTEM":
|
|
||||||
role = "USER"
|
|
||||||
elif role == "TOOL":
|
|
||||||
role = "USER"
|
|
||||||
|
|
||||||
content = m.get("content", "")
|
|
||||||
|
|
||||||
# Se vier lista (OpenAI multimodal), extrai texto
|
|
||||||
if isinstance(content, list):
|
|
||||||
parts = []
|
|
||||||
for item in content:
|
|
||||||
if isinstance(item, dict) and item.get("type") in ("text", "TEXT"):
|
|
||||||
parts.append(item.get("text", ""))
|
|
||||||
content = "\n".join(parts)
|
|
||||||
|
|
||||||
out.append({
|
|
||||||
"role": role,
|
|
||||||
"content": [{"type": "TEXT", "text": str(content)}]
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def call_oci_chat(body: dict):
|
|
||||||
signer = get_signer()
|
|
||||||
|
|
||||||
model = body.get("model")
|
|
||||||
oci_model = MODEL_MAP.get(model, model)
|
|
||||||
|
|
||||||
url = f"{OCI_GENAI_ENDPOINT}/20231130/actions/chat"
|
|
||||||
|
|
||||||
generic_messages = _openai_messages_to_generic(body.get("messages", []))
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"compartmentId": OCI_COMPARTMENT_ID,
|
|
||||||
"servingMode": {
|
|
||||||
"servingType": "ON_DEMAND",
|
|
||||||
"modelId": oci_model
|
|
||||||
},
|
|
||||||
"chatRequest": {
|
|
||||||
"apiFormat": "GENERIC",
|
|
||||||
"messages": generic_messages,
|
|
||||||
"maxTokens": int(body.get("max_tokens", 1024)),
|
|
||||||
"temperature": float(body.get("temperature", 0.0)),
|
|
||||||
"topP": float(body.get("top_p", 1.0)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ⚠️ IMPORTANTÍSSIMO:
|
|
||||||
# Em GENERIC, NÃO envie tools/tool_choice/stream (você orquestra tools no proxy)
|
|
||||||
# Se você mandar, pode dar 400 "correct format of request".
|
|
||||||
|
|
||||||
print("\n=== PAYLOAD FINAL (GENERIC) ===")
|
|
||||||
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
||||||
|
|
||||||
r = requests.post(url, json=payload, auth=signer)
|
|
||||||
if r.status_code != 200:
|
|
||||||
print("OCI ERROR:", r.text)
|
|
||||||
raise HTTPException(status_code=r.status_code, detail=r.text)
|
|
||||||
|
|
||||||
return r.json()["chatResponse"]
|
|
||||||
|
|
||||||
def detect_tool_call(text: str):
|
|
||||||
pattern = r"exec\s*\(\s*([^\s]+)\s*(.*?)\s*\)"
|
|
||||||
match = re.search(pattern, text)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
return None
|
|
||||||
|
|
||||||
tool_name = "exec"
|
|
||||||
command = match.group(1)
|
|
||||||
args = match.group(2)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"tool": tool_name,
|
|
||||||
"args_raw": f"{command} {args}".strip()
|
|
||||||
}
|
|
||||||
|
|
||||||
def execute_exec_command(command: str):
|
|
||||||
try:
|
|
||||||
print(f"LOG: EXEC COMMAND: {command}")
|
|
||||||
result = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT)
|
|
||||||
return result.decode()
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
return e.output.decode()
|
|
||||||
|
|
||||||
def execute_real_tool(name, args):
|
|
||||||
|
|
||||||
if name == "weather":
|
|
||||||
city = args.get("city")
|
|
||||||
return get_weather_from_api(city)
|
|
||||||
|
|
||||||
return "Tool not implemented"
|
|
||||||
|
|
||||||
def _extract_generic_text(oci_message: dict) -> str:
|
|
||||||
content = oci_message.get("content")
|
|
||||||
if isinstance(content, list):
|
|
||||||
return "".join([i.get("text", "") for i in content if isinstance(i, dict) and i.get("type") == "TEXT"])
|
|
||||||
if isinstance(content, str):
|
|
||||||
return content
|
|
||||||
return str(content)
|
|
||||||
|
|
||||||
|
|
||||||
def agent_loop(body: dict, max_iterations=5):
|
|
||||||
|
|
||||||
# Trabalhe sempre com OpenAI messages internamente,
|
|
||||||
# mas call_oci_chat converte pra GENERIC.
|
|
||||||
messages = []
|
|
||||||
messages.append({"role": "system", "content": SYSTEM_AGENT_PROMPT})
|
|
||||||
messages.extend(body.get("messages", []))
|
|
||||||
|
|
||||||
for _ in range(max_iterations):
|
|
||||||
|
|
||||||
response = call_oci_chat({**body, "messages": messages})
|
|
||||||
|
|
||||||
oci_choice = response["choices"][0]
|
|
||||||
oci_message = oci_choice["message"]
|
|
||||||
|
|
||||||
text = _extract_generic_text(oci_message)
|
|
||||||
|
|
||||||
try:
|
|
||||||
agent_output = json.loads(text)
|
|
||||||
except:
|
|
||||||
# modelo não retornou JSON (quebrou regra)
|
|
||||||
return response
|
|
||||||
|
|
||||||
if agent_output.get("action") == "call_tool":
|
|
||||||
tool_name = agent_output.get("tool")
|
|
||||||
args = agent_output.get("arguments", {})
|
|
||||||
|
|
||||||
if tool_name not in TOOLS:
|
|
||||||
# devolve pro modelo como erro
|
|
||||||
messages.append({"role": "assistant", "content": text})
|
|
||||||
messages.append({"role": "user", "content": json.dumps({
|
|
||||||
"tool_error": f"Tool '{tool_name}' not implemented"
|
|
||||||
})})
|
|
||||||
continue
|
|
||||||
|
|
||||||
tool_result = TOOLS[tool_name](**args)
|
|
||||||
|
|
||||||
# Mantém o histórico: (1) decisão do agente, (2) resultado do tool
|
|
||||||
messages.append({"role": "assistant", "content": text})
|
|
||||||
messages.append({"role": "user", "content": json.dumps({
|
|
||||||
"tool_result": {
|
|
||||||
"tool": tool_name,
|
|
||||||
"arguments": args,
|
|
||||||
"result": tool_result
|
|
||||||
}
|
|
||||||
}, ensure_ascii=False)})
|
|
||||||
|
|
||||||
continue
|
|
||||||
|
|
||||||
if agent_output.get("action") == "final_answer":
|
|
||||||
return response
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# STREAMING ADAPTER
|
# STREAMING ADAPTER
|
||||||
@@ -387,35 +559,12 @@ async def chat_completions(request: Request):
|
|||||||
# chat_response = agent_loop(body)
|
# chat_response = agent_loop(body)
|
||||||
|
|
||||||
if OPENCLAW_TOOLS_ACTIVE:
|
if OPENCLAW_TOOLS_ACTIVE:
|
||||||
chat_response = call_oci_chat(body)
|
chat_response = run_exec_loop(body, max_steps=10000)
|
||||||
|
|
||||||
oci_choice = chat_response["choices"][0]
|
|
||||||
oci_message = oci_choice["message"]
|
|
||||||
|
|
||||||
content_text = _extract_generic_text(oci_message)
|
|
||||||
|
|
||||||
# 🔥 DETECT EXEC
|
|
||||||
exec_match = re.search(r"\(exec\s+(.*?)\)", content_text)
|
|
||||||
|
|
||||||
if exec_match:
|
|
||||||
command = exec_match.group(1)
|
|
||||||
result = execute_exec_command(command)
|
|
||||||
|
|
||||||
# Injeta resultado e chama novamente
|
|
||||||
new_messages = body["messages"] + [
|
|
||||||
{"role": "assistant", "content": content_text},
|
|
||||||
{"role": "user", "content": f"Tool result:\n{result}"}
|
|
||||||
]
|
|
||||||
|
|
||||||
chat_response = call_oci_chat({
|
|
||||||
**body,
|
|
||||||
"messages": new_messages
|
|
||||||
})
|
|
||||||
else:
|
else:
|
||||||
# 🔥 Modo enterprise → seu agent_loop controla tools
|
# 🔥 Modo enterprise → seu agent_loop controla tools
|
||||||
chat_response = agent_loop(body)
|
chat_response = agent_loop(body)
|
||||||
|
|
||||||
print("FINAL RESPONSE:", json.dumps(chat_response, indent=2))
|
# print("FINAL RESPONSE:", json.dumps(chat_response, indent=2))
|
||||||
|
|
||||||
oci_choice = chat_response["choices"][0]
|
oci_choice = chat_response["choices"][0]
|
||||||
oci_message = oci_choice["message"]
|
oci_message = oci_choice["message"]
|
||||||
@@ -566,15 +715,15 @@ async def responses(request: Request):
|
|||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def log_requests(request: Request, call_next):
|
async def log_requests(request: Request, call_next):
|
||||||
print("\n>>> ENDPOINT:", request.method, request.url.path)
|
# print("\n>>> ENDPOINT:", request.method, request.url.path)
|
||||||
|
|
||||||
body = await request.body()
|
body = await request.body()
|
||||||
try:
|
try:
|
||||||
body_json = json.loads(body.decode())
|
body_json = json.loads(body.decode())
|
||||||
print(">>> BODY:", json.dumps(body_json, indent=2))
|
# print(">>> BODY:", json.dumps(body_json, indent=2))
|
||||||
except:
|
except:
|
||||||
print(">>> BODY RAW:", body.decode())
|
print(">>> BODY RAW:", body.decode())
|
||||||
|
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
print(">>> STATUS:", response.status_code)
|
# print(">>> STATUS:", response.status_code)
|
||||||
return response
|
return response
|
||||||
Reference in New Issue
Block a user