refactor: clean up legacy code, rename hopper to entity_extractor, and add third-party notices

- Add NOTICE file and README license references for mobile-use and finalrun-agent
- Add derivative attribution headers to 17 files derived from mobile-use
- Rename hopper agent to entity_extractor across prompts, tools, and configs
- Remove obsolete adb_tunnel, ApiBaseUrl, and legacy CLI utility scripts
- Replace legacy examples with native Artemis diagnostic and workflow examples
- Update unit and tool tests to validate entity_extractor and SDK builders
This commit is contained in:
somew1nd committed 2026-09-11 16:19:42 -07:00
1 parent 0860788192
commit 54fcef9887
83 files changed
+1996 -3660

No files matched your search

@@ -0,0 +1,25 @@
# 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.
"""Entity extractor and package resolver agent for ARTEMIS."""
from artemis.agents.entity_extractor.entity_extractor import (
ExtractionResult,
extract_entity,
)
__all__ = [
"ExtractionResult",
"extract_entity",
]
@@ -0,0 +1,30 @@
# Artemis Entity Extractor & Package Resolver
You are an autonomous, high-precision structured data extractor in the ARTEMIS mobile testing runtime.
Your responsibility is to extract the exact requested identifier, package name, or configuration token from raw device dump outputs (such as `pm list packages`, `dumpsys`, logcat records, or accessibility tree dumps).
## Operating Directives
1. **Verbatim Extraction**:
- Return the extracted entity exactly as it appears in the source data.
- Do NOT modify capitalization, strip domain segments, translate, or normalize the matched identifier.
2. **Package Resolution Rules**:
- Match target application titles, codenames, or brand identifiers against standard Android package formats (`com.<vendor>.<app>`, `org.<project>.<module>`, etc.).
- Prioritize exact matches over partial or prefix matches.
3. **Strict Disambiguation & Null-Safety**:
- If the requested entity is absent from the input dataset, return `found: false` with `output: null`.
- If multiple candidates conflict and cannot be deterministically verified from the query, do NOT guess. Set `found: false` and `output: null`.
## Structured Deliverable Schema
You must produce your final answer conforming to the following JSON structure:
```json
{
"found": true,
"output": "<exact_matched_string_or_null>",
"reason": "<one_sentence_rationale_confirming_criteria>"
}
```
@@ -0,0 +1,90 @@
# 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.
"""Lightweight entity extraction and package resolver for ARTEMIS.
Dispatches bounded LLM extraction turns to identify application package identifiers
and configuration attributes from raw device listings without manual parsing heuristics.
"""
from __future__ import annotations
from pathlib import Path
from typing import cast
from jinja2 import Template
from langchain_core.messages import HumanMessage, SystemMessage
from pydantic import BaseModel, Field
from artemis.context import ArtemisContext
from artemis.data_engine.trace import trace
from artemis.services.llm import get_llm, invoke_llm_with_timeout_message, with_fallback
from artemis.utils.logger import get_logger
logger = get_logger(__name__)
class ExtractionResult(BaseModel):
"""Structured extraction outcome for entity resolution."""
found: bool = Field(description="True if the requested entity was definitively identified.")
output: str | None = Field(default=None, description="Exact extracted identifier or string value.")
reason: str = Field(description="Brief justification of the match criteria applied.")
@trace(type="agent", name="entity_extractor")
async def extract_entity(
ctx: ArtemisContext,
request: str,
data: str,
use_fallback: bool = True,
) -> ExtractionResult:
"""Extract a specific identifier or entity from raw batch device data."""
logger.info(f"Invoking Artemis Entity Extractor (fallback={use_fallback})")
template_path = Path(__file__).parent / "entity_extractor.md"
system_prompt = Template(template_path.read_text(encoding="utf-8")).render()
messages = [
SystemMessage(content=system_prompt),
HumanMessage(content=f"Target Query:\n{request}\n\nCandidate Raw Dataset:\n{data}"),
]
primary_llm = get_llm(ctx=ctx, name="entity_extractor", is_utils=True).with_structured_output(
ExtractionResult
)
try:
if use_fallback:
fallback_llm = get_llm(
ctx=ctx, name="entity_extractor", is_utils=True, use_fallback=True
).with_structured_output(ExtractionResult)
raw = await with_fallback(
main_call=lambda: invoke_llm_with_timeout_message(primary_llm.ainvoke(messages)),
fallback_call=lambda: invoke_llm_with_timeout_message(fallback_llm.ainvoke(messages)),
)
else:
raw = await invoke_llm_with_timeout_message(primary_llm.ainvoke(messages))
if isinstance(raw, ExtractionResult):
return raw
if isinstance(raw, dict):
return ExtractionResult.model_validate(raw)
return cast(ExtractionResult, raw)
except Exception as exc:
logger.error(f"Entity extraction turn failed: {exc}")
return ExtractionResult(
found=False,
output=None,
reason=f"LLM extraction error: {str(exc)[:400]}",
)
-15
View File
@@ -1,15 +0,0 @@
## Hopper
Extract relevant information from batch data. **Keep extracted data exactly as-is** - no reformatting.
## Output
- **found**: `true` if data was found, `false` otherwise
- **output**: Extracted information if found, `null` otherwise
- **reason**: Brief explanation of search logic
## Rules
1. **Search entire input** - may contain hundreds of entries
2. **For app package lookup**: Match app name (or variations) in package identifier
- Common patterns: lowercase app name, company+app, brand name, codenames
3. **Prefer direct matches** over partial matches
4. **Return `null`** if not found or if multiple ambiguous matches exist - don't guess
-93
View File
@@ -1,93 +0,0 @@
# 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.
from pathlib import Path
from jinja2 import Template
from langchain_core.messages import HumanMessage, SystemMessage
from artemis.context import ArtemisContext
from artemis.data_engine.trace import trace
from artemis.services.llm import get_llm, invoke_llm_with_timeout_message, with_fallback
from artemis.utils.logger import get_logger
from pydantic import BaseModel, Field
logger = get_logger(__name__)
class _CyFunctionDetectorMeta(type):
def __instancecheck__(self, instance):
name = type(instance).__name__
return (
name
in (
"cyfunction",
"cython_function_or_method",
"builtin_function_or_method",
)
or "cyfunction" in name.lower()
)
class CyFunctionDetector(metaclass=_CyFunctionDetectorMeta):
pass
class HopperOutput(BaseModel):
model_config = {"ignored_types": (CyFunctionDetector,)}
found: bool = Field(description="True if the requested data was found, False otherwise.")
output: str | None = Field(description="The extracted data if found, null otherwise.")
reason: str = Field(
description="A short explanation of what you looked for"
+ " and how you decided what to extract."
)
@trace(type="agent", name="hopper")
async def hopper(
ctx: ArtemisContext,
request: str,
data: str,
use_fallback: bool = True,
) -> HopperOutput:
logger.info(f"Starting Hopper Agent (use_fallback={use_fallback})")
system_message = Template(
Path(__file__).parent.joinpath("hopper.md").read_text(encoding="utf-8")
).render()
messages = [
SystemMessage(content=system_message),
HumanMessage(content=f"{request}\nHere is the data you must dig:\n{data}"),
]
llm = get_llm(ctx=ctx, name="hopper", is_utils=True).with_structured_output(HopperOutput)
try:
if use_fallback:
llm_fallback = get_llm(
ctx=ctx, name="hopper", is_utils=True, use_fallback=True
).with_structured_output(HopperOutput)
response: HopperOutput = await with_fallback(
main_call=lambda: invoke_llm_with_timeout_message(llm.ainvoke(messages)),
fallback_call=lambda: invoke_llm_with_timeout_message(
llm_fallback.ainvoke(messages)
),
) # type: ignore
else:
response: HopperOutput = await invoke_llm_with_timeout_message(llm.ainvoke(messages))
return response
except Exception as e:
logger.error(f"Hopper LLM invocation failed: {e}")
return HopperOutput(
found=False,
output=None,
reason=f"Failed due to LLM error: {str(e)[:500]}",
)
@@ -21,26 +21,7 @@ from langchain_core.tools import BaseTool, StructuredTool
from pydantic import BaseModel, Field
class _CyFunctionDetectorMeta(type):
def __instancecheck__(self, instance):
name = type(instance).__name__
return (
name
in (
"cyfunction",
"cython_function_or_method",
"builtin_function_or_method",
)
or "cyfunction" in name.lower()
)
class CyFunctionDetector(metaclass=_CyFunctionDetectorMeta):
pass
class SpawnLogReaderArgs(BaseModel):
model_config = {"ignored_types": (CyFunctionDetector,)}
specific_query: str = Field(
...,
description=(
@@ -54,7 +35,6 @@ from langgraph.prebuilt import InjectedState
from artemis.core.tool_failure import ToolFailure, is_tool_failure
from artemis.context import ArtemisContext
from artemis.data_engine.trace import (
CURRENT_TRACE_ID,
TraceSpan,
trace,
)
+3
View File
@@ -11,6 +11,9 @@
# 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.
#
# Portions of this file are derived from mobile-use (https://github.com/minitap-ai/mobile-use)
# Copyright 2025-2026 Minitap, Inc. Licensed under the Apache License 2.0.
import json
from pathlib import Path
+1 -20
View File
@@ -23,7 +23,7 @@ from pydantic import BaseModel, Field
from artemis.context import ArtemisContext
from artemis.controllers.unified_controller import UnifiedMobileController
from artemis.data_engine.trace import CURRENT_TRACE_ID, TraceSpan, trace
from artemis.data_engine.trace import TraceSpan, trace
from artemis.graph.state import State
from artemis.graph.visibility import strict_state
from artemis.services.llm import (
@@ -108,26 +108,7 @@ def build_planner_system_blocks(prompts_data: dict, mode: str, include_checks: b
return blocks
class _CyFunctionDetectorMeta(type):
def __instancecheck__(self, instance):
name = type(instance).__name__
return (
name
in (
"cyfunction",
"cython_function_or_method",
"builtin_function_or_method",
)
or "cyfunction" in name.lower()
)
class CyFunctionDetector(metaclass=_CyFunctionDetectorMeta):
pass
class ValidationResult(BaseModel):
model_config = {"ignored_types": (CyFunctionDetector,)}
is_approved: bool = Field(
description=(
"True if the new plan still serves the initial goal and is coherent."