feat: enable remote SDK and resilient device orchestration
This commit is contained in:
1 parent
3cc711df51
commit
ddfa420945
110 files changed
+14450
-12954
No files matched your search
@@ -0,0 +1,179 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Fake chat model for infrastructure testing (ARTEMIS_FAKE_LLM=1).
|
||||
|
||||
Completes any task on the first LLM invocation so that queueing, entry
|
||||
points, device locking, and status propagation can be exercised without
|
||||
real model calls:
|
||||
|
||||
- Flash (report_task_status bound): immediately reports status=completed.
|
||||
- Pro planner (save_note bound): saves a task plan whose only subgoal is
|
||||
already checked, so convergence_gate ends the graph after planning.
|
||||
- Anything else: returns a plain "FAKE_LLM_OK" text message.
|
||||
|
||||
ARTEMIS_FAKE_LLM_DELAY_S adds an artificial per-call delay so tasks stay
|
||||
alive long enough to observe queue/concurrency behavior.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from typing import Any, Iterator, AsyncIterator
|
||||
import uuid
|
||||
|
||||
from langchain_core.callbacks import (
|
||||
AsyncCallbackManagerForLLMRun,
|
||||
CallbackManagerForLLMRun,
|
||||
)
|
||||
from langchain_core.language_models.chat_models import BaseChatModel
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk, BaseMessage
|
||||
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
|
||||
|
||||
FAKE_DONE_TEXT = "FAKE_LLM: task auto-completed for infrastructure testing."
|
||||
|
||||
|
||||
def _tool_name(tool: Any) -> str | None:
|
||||
name = getattr(tool, "name", None)
|
||||
if isinstance(name, str):
|
||||
return name
|
||||
if isinstance(tool, dict):
|
||||
if isinstance(tool.get("name"), str):
|
||||
return tool["name"]
|
||||
fn = tool.get("function")
|
||||
if isinstance(fn, dict) and isinstance(fn.get("name"), str):
|
||||
return fn["name"]
|
||||
return None
|
||||
|
||||
|
||||
class FakeChatModel(BaseChatModel):
|
||||
"""Deterministic fake model that finishes every task on the first call."""
|
||||
|
||||
bound_tool_names: list[str] = []
|
||||
delay_s: float = 0.0
|
||||
|
||||
@property
|
||||
def _llm_type(self) -> str:
|
||||
return "artemis-fake"
|
||||
|
||||
def bind_tools(self, tools: Any, **kwargs: Any) -> "FakeChatModel":
|
||||
names = [n for n in (_tool_name(t) for t in tools) if n]
|
||||
return FakeChatModel(bound_tool_names=names, delay_s=self.delay_s)
|
||||
|
||||
def with_structured_output(self, schema: Any, **kwargs: Any) -> "FakeChatModel":
|
||||
return self
|
||||
|
||||
def _decide(self, messages: list[BaseMessage]) -> AIMessage:
|
||||
call_id = f"fake-{uuid.uuid4().hex[:12]}"
|
||||
usage = {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2}
|
||||
|
||||
if "report_task_status" in self.bound_tool_names:
|
||||
return AIMessage(
|
||||
content="Task verified complete on first observation.",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "report_task_status",
|
||||
"args": {"status": "completed", "explanation": FAKE_DONE_TEXT},
|
||||
"id": call_id,
|
||||
}
|
||||
],
|
||||
usage_metadata=usage,
|
||||
)
|
||||
|
||||
if "save_note" in self.bound_tool_names:
|
||||
already_planned = any(
|
||||
tc.get("name") == "save_note"
|
||||
for m in messages
|
||||
for tc in (getattr(m, "tool_calls", None) or [])
|
||||
)
|
||||
if already_planned:
|
||||
return AIMessage(content="Plan saved. " + FAKE_DONE_TEXT, usage_metadata=usage)
|
||||
return AIMessage(
|
||||
content="Saving completed plan.",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "save_note",
|
||||
"args": {
|
||||
"key": "task_plan",
|
||||
"content": "- [x] " + FAKE_DONE_TEXT,
|
||||
},
|
||||
"id": call_id,
|
||||
}
|
||||
],
|
||||
usage_metadata=usage,
|
||||
)
|
||||
|
||||
return AIMessage(content=FAKE_DONE_TEXT, usage_metadata=usage)
|
||||
|
||||
def _to_chunk(self, msg: AIMessage) -> ChatGenerationChunk:
|
||||
tool_call_chunks = [
|
||||
{
|
||||
"name": tc["name"],
|
||||
"args": json.dumps(tc["args"]),
|
||||
"id": tc["id"],
|
||||
"index": i,
|
||||
"type": "tool_call_chunk",
|
||||
}
|
||||
for i, tc in enumerate(msg.tool_calls or [])
|
||||
]
|
||||
chunk = AIMessageChunk(
|
||||
content=msg.content,
|
||||
tool_call_chunks=tool_call_chunks,
|
||||
usage_metadata=msg.usage_metadata,
|
||||
)
|
||||
return ChatGenerationChunk(message=chunk)
|
||||
|
||||
def _generate(
|
||||
self,
|
||||
messages: list[BaseMessage],
|
||||
stop: list[str] | None = None,
|
||||
run_manager: CallbackManagerForLLMRun | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatResult:
|
||||
if self.delay_s > 0:
|
||||
time.sleep(self.delay_s)
|
||||
return ChatResult(generations=[ChatGeneration(message=self._decide(messages))])
|
||||
|
||||
async def _agenerate(
|
||||
self,
|
||||
messages: list[BaseMessage],
|
||||
stop: list[str] | None = None,
|
||||
run_manager: AsyncCallbackManagerForLLMRun | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatResult:
|
||||
if self.delay_s > 0:
|
||||
await asyncio.sleep(self.delay_s)
|
||||
return ChatResult(generations=[ChatGeneration(message=self._decide(messages))])
|
||||
|
||||
def _stream(
|
||||
self,
|
||||
messages: list[BaseMessage],
|
||||
stop: list[str] | None = None,
|
||||
run_manager: CallbackManagerForLLMRun | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Iterator[ChatGenerationChunk]:
|
||||
if self.delay_s > 0:
|
||||
time.sleep(self.delay_s)
|
||||
yield self._to_chunk(self._decide(messages))
|
||||
|
||||
async def _astream(
|
||||
self,
|
||||
messages: list[BaseMessage],
|
||||
stop: list[str] | None = None,
|
||||
run_manager: AsyncCallbackManagerForLLMRun | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[ChatGenerationChunk]:
|
||||
if self.delay_s > 0:
|
||||
await asyncio.sleep(self.delay_s)
|
||||
yield self._to_chunk(self._decide(messages))
|
||||
@@ -188,6 +188,16 @@ class ModelFactory:
|
||||
|
||||
@classmethod
|
||||
def create_model(cls, endpoint: ModelEndpoint) -> BaseChatModel:
|
||||
if os.environ.get("ARTEMIS_FAKE_LLM") == "1":
|
||||
from artemis.llm.fake_model import FakeChatModel
|
||||
|
||||
delay = float(os.environ.get("ARTEMIS_FAKE_LLM_DELAY_S", "0") or 0)
|
||||
logger.warning(
|
||||
f"ARTEMIS_FAKE_LLM=1 — returning FakeChatModel (delay={delay}s) "
|
||||
f"instead of {endpoint.provider}/{endpoint.model_name}"
|
||||
)
|
||||
return FakeChatModel(delay_s=delay)
|
||||
|
||||
provider = ModelProvider.from_string(endpoint.provider)
|
||||
|
||||
if provider == ModelProvider.GOOGLE:
|
||||
|
||||
Reference in new issue
Block a user