favlo 0.3.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.
- favlo/__init__.py +3 -0
- favlo/__main__.py +3 -0
- favlo/chat_client.py +117 -0
- favlo/cli.py +225 -0
- favlo/config_tools.py +234 -0
- favlo/console.py +1175 -0
- favlo/daemon.py +496 -0
- favlo/embeddings_pipeline.py +105 -0
- favlo/guides.py +49 -0
- favlo/hf_cache.py +31 -0
- favlo/http_app.py +924 -0
- favlo/images.py +130 -0
- favlo/language_pipeline.py +86 -0
- favlo/listener.py +342 -0
- favlo/logs.py +293 -0
- favlo/machine.py +237 -0
- favlo/mcp_server.py +969 -0
- favlo/media_meta.py +118 -0
- favlo/media_pipeline.py +454 -0
- favlo/media_session.py +67 -0
- favlo/memory.py +996 -0
- favlo/memory_mcp.py +111 -0
- favlo/memory_notes.py +322 -0
- favlo/music.py +151 -0
- favlo/offset.py +117 -0
- favlo/persona.py +17 -0
- favlo/pipeline.py +69 -0
- favlo/pipeline_worker.py +877 -0
- favlo/pipelines.py +565 -0
- favlo/prefix.py +695 -0
- favlo/registry.py +999 -0
- favlo/reply.py +128 -0
- favlo/schedule.py +186 -0
- favlo/setup.py +75 -0
- favlo/setup_run.py +271 -0
- favlo/smoke_lock.py +305 -0
- favlo/videos.py +223 -0
- favlo/voice.py +151 -0
- favlo/web_host.py +143 -0
- favlo-0.3.0.dist-info/METADATA +164 -0
- favlo-0.3.0.dist-info/RECORD +45 -0
- favlo-0.3.0.dist-info/WHEEL +4 -0
- favlo-0.3.0.dist-info/entry_points.txt +2 -0
- favlo-0.3.0.dist-info/licenses/LICENSE.md +202 -0
- favlo-0.3.0.dist-info/licenses/NOTICE +5 -0
favlo/__init__.py
ADDED
favlo/__main__.py
ADDED
favlo/chat_client.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Agent Framework client for the listener's chat completions.
|
|
2
|
+
|
|
3
|
+
Every chat caller uses this client. Reasoning from a completion is sent back
|
|
4
|
+
on ``reasoning_content``. A reply whose text already contains the tool-call
|
|
5
|
+
markup is sent as that text.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def reasoning_text(message: Any) -> str:
|
|
14
|
+
"""``reasoning_content`` on a completion message or a stream delta."""
|
|
15
|
+
if message is None:
|
|
16
|
+
return ""
|
|
17
|
+
value = getattr(message, "reasoning_content", None)
|
|
18
|
+
if isinstance(value, str) and value:
|
|
19
|
+
return value
|
|
20
|
+
extra = getattr(message, "model_extra", None)
|
|
21
|
+
if isinstance(extra, dict):
|
|
22
|
+
raw = extra.get("reasoning_content")
|
|
23
|
+
if isinstance(raw, str) and raw:
|
|
24
|
+
return raw
|
|
25
|
+
return ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def read_reasoning(message: Any, contents: list[Any]) -> list[Any]:
|
|
29
|
+
"""Keep ``reasoning_content`` as reasoning, beside the reply text."""
|
|
30
|
+
text = reasoning_text(message)
|
|
31
|
+
if not text:
|
|
32
|
+
return contents
|
|
33
|
+
from agent_framework import Content
|
|
34
|
+
|
|
35
|
+
return [Content.from_text_reasoning(text=text), *contents]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def prepare_messages(message: object, built: list[dict[str, object]]) -> list[dict[str, object]]:
|
|
39
|
+
"""Put reasoning on ``reasoning_content`` for the chat template.
|
|
40
|
+
|
|
41
|
+
The listener reads that field when it renders the next prompt. A reply
|
|
42
|
+
whose text already contains the tool-call markup is sent as that text.
|
|
43
|
+
"""
|
|
44
|
+
if getattr(message, "role", None) != "assistant":
|
|
45
|
+
return built
|
|
46
|
+
reasoning = "".join(
|
|
47
|
+
part.text or ""
|
|
48
|
+
for part in getattr(message, "contents", ())
|
|
49
|
+
if getattr(part, "type", None) == "text_reasoning"
|
|
50
|
+
and not getattr(part, "protected_data", None)
|
|
51
|
+
)
|
|
52
|
+
if not reasoning:
|
|
53
|
+
return built
|
|
54
|
+
kept: list[dict[str, object]] = []
|
|
55
|
+
placed = False
|
|
56
|
+
for item in built:
|
|
57
|
+
if item.get("role") != "assistant":
|
|
58
|
+
kept.append(item)
|
|
59
|
+
continue
|
|
60
|
+
text = _content_text(item.get("content"))
|
|
61
|
+
if text == reasoning and "tool_calls" not in item:
|
|
62
|
+
continue
|
|
63
|
+
copied = dict(item)
|
|
64
|
+
if not placed:
|
|
65
|
+
copied["reasoning_content"] = reasoning
|
|
66
|
+
placed = True
|
|
67
|
+
if "<tool_call>" in text:
|
|
68
|
+
copied.pop("tool_calls", None)
|
|
69
|
+
kept.append(copied)
|
|
70
|
+
if not placed:
|
|
71
|
+
kept.append({"role": "assistant", "reasoning_content": reasoning, "content": ""})
|
|
72
|
+
return kept
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def history_message(role: str, text: str, reasoning: str = "") -> Any:
|
|
76
|
+
"""One stored turn. Reasoning stays a reasoning part, not reply text."""
|
|
77
|
+
from agent_framework import Content, Message
|
|
78
|
+
|
|
79
|
+
parts: list[Any] = []
|
|
80
|
+
if reasoning:
|
|
81
|
+
parts.append(Content.from_text_reasoning(text=reasoning))
|
|
82
|
+
parts.append(text)
|
|
83
|
+
return Message(role, parts)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def open_chat_client(
|
|
87
|
+
model: str,
|
|
88
|
+
base_url: str,
|
|
89
|
+
async_client: Any | None = None,
|
|
90
|
+
) -> Any:
|
|
91
|
+
"""Chat client that sends reasoning back on the next request."""
|
|
92
|
+
from agent_framework.openai import OpenAIChatCompletionClient
|
|
93
|
+
|
|
94
|
+
kwargs: dict[str, Any] = {
|
|
95
|
+
"model": model,
|
|
96
|
+
"api_key": "favlo",
|
|
97
|
+
"base_url": base_url,
|
|
98
|
+
"response_parser": read_reasoning,
|
|
99
|
+
"message_preparer": prepare_messages,
|
|
100
|
+
}
|
|
101
|
+
if async_client is not None:
|
|
102
|
+
kwargs["async_client"] = async_client
|
|
103
|
+
return OpenAIChatCompletionClient(**kwargs)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _content_text(content: object) -> str:
|
|
107
|
+
if isinstance(content, str):
|
|
108
|
+
return content
|
|
109
|
+
if not isinstance(content, list):
|
|
110
|
+
return ""
|
|
111
|
+
parts: list[str] = []
|
|
112
|
+
for item in content:
|
|
113
|
+
if isinstance(item, str):
|
|
114
|
+
parts.append(item)
|
|
115
|
+
elif isinstance(item, dict) and item.get("type") == "text":
|
|
116
|
+
parts.append(str(item.get("text") or ""))
|
|
117
|
+
return "".join(parts)
|
favlo/cli.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Select one pipeline file and run it.
|
|
2
|
+
|
|
3
|
+
A model flag is the filename without ``.py``. ``--<pipeline>`` loads
|
|
4
|
+
``<pipeline>.py`` by path, and that file loads the model's package.
|
|
5
|
+
The hyphenated file is not an import.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import importlib.util
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
from collections.abc import Callable, Sequence
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
|
|
19
|
+
RUNTIMES = ("metal41", "cuda")
|
|
20
|
+
_IMPORTS = threading.Lock()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def repo_root() -> Path:
|
|
24
|
+
override = os.environ.get("FAVLO_ROOT")
|
|
25
|
+
if override:
|
|
26
|
+
return Path(override)
|
|
27
|
+
for parent in Path(__file__).resolve().parents:
|
|
28
|
+
marker = parent / "src" / "favlo" / "cli.py"
|
|
29
|
+
if (parent / "pyproject.toml").is_file() and marker.is_file():
|
|
30
|
+
return parent
|
|
31
|
+
message = "favlo package is not inside a checkout"
|
|
32
|
+
raise FileNotFoundError(message)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def pipeline_tree(runtime: str, *, custom: bool = False, root: Path | None = None) -> Path:
|
|
36
|
+
"""The directory that holds one runtime's entry files."""
|
|
37
|
+
if runtime not in RUNTIMES:
|
|
38
|
+
message = f"unknown runtime: {runtime}"
|
|
39
|
+
raise ValueError(message)
|
|
40
|
+
base = "custom-pipelines" if custom else "pipelines"
|
|
41
|
+
return (root or repo_root()) / base / runtime
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def pipeline_names(root: Path | None = None) -> tuple[str, ...]:
|
|
45
|
+
"""Entry stems under the four pipeline trees.
|
|
46
|
+
|
|
47
|
+
A new runtime is a new file. Discovering it does not edit this module.
|
|
48
|
+
Two files with the same ``PIPELINE`` stem are an error.
|
|
49
|
+
"""
|
|
50
|
+
return tuple(sorted(pipeline_entries(root)))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def pipeline_entries(root: Path | None = None) -> dict[str, Path]:
|
|
54
|
+
"""Stem to entry file. A duplicate stem raises ``ValueError``."""
|
|
55
|
+
found: dict[str, Path] = {}
|
|
56
|
+
for custom in (False, True):
|
|
57
|
+
for runtime in RUNTIMES:
|
|
58
|
+
directory = pipeline_tree(runtime, custom=custom, root=root)
|
|
59
|
+
if not directory.is_dir():
|
|
60
|
+
continue
|
|
61
|
+
for path in sorted(directory.glob("*.py")):
|
|
62
|
+
stem = _pipeline_stem(path)
|
|
63
|
+
if stem is None:
|
|
64
|
+
continue
|
|
65
|
+
previous = found.get(stem)
|
|
66
|
+
if previous is not None:
|
|
67
|
+
message = f"duplicate pipeline {stem}: {previous} and {path}"
|
|
68
|
+
raise ValueError(message)
|
|
69
|
+
found[stem] = path
|
|
70
|
+
return found
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _pipeline_stem(path: Path) -> str | None:
|
|
74
|
+
try:
|
|
75
|
+
tree = ast.parse(path.read_text(encoding="utf-8"))
|
|
76
|
+
except (OSError, SyntaxError):
|
|
77
|
+
return None
|
|
78
|
+
for node in tree.body:
|
|
79
|
+
if not isinstance(node, ast.Assign) or len(node.targets) != 1:
|
|
80
|
+
continue
|
|
81
|
+
target = node.targets[0]
|
|
82
|
+
if not isinstance(target, ast.Name) or target.id != "PIPELINE":
|
|
83
|
+
continue
|
|
84
|
+
if isinstance(node.value, ast.Constant) and node.value.value == path.stem:
|
|
85
|
+
return path.stem
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _help(names: Sequence[str]) -> str:
|
|
90
|
+
lines = ["usage: favlo serve [--non-interactive]"]
|
|
91
|
+
lines.append(" favlo web")
|
|
92
|
+
lines.extend(f" favlo --{name} [args]" for name in names)
|
|
93
|
+
lines.append("")
|
|
94
|
+
lines.append(" serve listen on this terminal")
|
|
95
|
+
lines.append(" serve --non-interactive")
|
|
96
|
+
lines.append(" start the server daemon and exit")
|
|
97
|
+
lines.append(" web start the API and the Expo app")
|
|
98
|
+
width = max((len(name) for name in names), default=0)
|
|
99
|
+
for name in names:
|
|
100
|
+
lines.append(f" --{name.ljust(width)} {name}")
|
|
101
|
+
return "\n".join(lines) + "\n"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def load_pipeline(
|
|
105
|
+
name: str,
|
|
106
|
+
root: Path | None = None,
|
|
107
|
+
*,
|
|
108
|
+
runtime: str | None = None,
|
|
109
|
+
custom: bool = False,
|
|
110
|
+
) -> ModuleType:
|
|
111
|
+
if runtime is None:
|
|
112
|
+
path = pipeline_entries(root).get(name)
|
|
113
|
+
if path is None:
|
|
114
|
+
message = f"pipeline file is missing: {name}"
|
|
115
|
+
raise FileNotFoundError(message)
|
|
116
|
+
else:
|
|
117
|
+
path = pipeline_tree(runtime, custom=custom, root=root) / f"{name}.py"
|
|
118
|
+
if not path.is_file():
|
|
119
|
+
message = f"pipeline file is missing: {path}"
|
|
120
|
+
raise FileNotFoundError(message)
|
|
121
|
+
with _IMPORTS:
|
|
122
|
+
spec = importlib.util.spec_from_file_location(f"{name.replace('-', '_')}_pipeline", path)
|
|
123
|
+
if spec is None or spec.loader is None:
|
|
124
|
+
message = f"cannot load pipeline file: {path}"
|
|
125
|
+
raise ImportError(message)
|
|
126
|
+
module = importlib.util.module_from_spec(spec)
|
|
127
|
+
spec.loader.exec_module(module)
|
|
128
|
+
return module
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def pipeline_roots(root: Path | None = None) -> tuple[Path, ...]:
|
|
132
|
+
"""Pipeline trees that exist in this checkout."""
|
|
133
|
+
found: list[Path] = []
|
|
134
|
+
for custom in (False, True):
|
|
135
|
+
for runtime in RUNTIMES:
|
|
136
|
+
directory = pipeline_tree(runtime, custom=custom, root=root)
|
|
137
|
+
if directory.is_dir():
|
|
138
|
+
found.append(directory.resolve())
|
|
139
|
+
return tuple(found)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def drop_pipeline_modules(root: Path | None = None) -> int:
|
|
143
|
+
"""Drop imported pipeline packages so the next load reads the files again.
|
|
144
|
+
|
|
145
|
+
The lock is the same one ``load_pipeline`` holds. A module under ``src/favlo`` stays.
|
|
146
|
+
"""
|
|
147
|
+
with _IMPORTS:
|
|
148
|
+
roots = pipeline_roots(root)
|
|
149
|
+
names = [
|
|
150
|
+
name
|
|
151
|
+
for name, module in list(sys.modules.items())
|
|
152
|
+
if module is not None and _module_under(module, roots)
|
|
153
|
+
]
|
|
154
|
+
for name in names:
|
|
155
|
+
sys.modules.pop(name, None)
|
|
156
|
+
return len(names)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _module_under(module: ModuleType, roots: tuple[Path, ...]) -> bool:
|
|
160
|
+
file = getattr(module, "__file__", None)
|
|
161
|
+
if isinstance(file, str) and file and _path_under(Path(file), roots):
|
|
162
|
+
return True
|
|
163
|
+
paths = getattr(module, "__path__", None)
|
|
164
|
+
if paths is None:
|
|
165
|
+
return False
|
|
166
|
+
return any(_path_under(Path(str(item)), roots) for item in paths)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _path_under(path: Path, roots: tuple[Path, ...]) -> bool:
|
|
170
|
+
try:
|
|
171
|
+
resolved = path.resolve()
|
|
172
|
+
except OSError:
|
|
173
|
+
return False
|
|
174
|
+
return any(resolved.is_relative_to(root) for root in roots)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def pipeline_main(module: ModuleType) -> Callable[[Sequence[str]], int]:
|
|
178
|
+
entry = getattr(module, "main", None)
|
|
179
|
+
if not callable(entry):
|
|
180
|
+
message = f"{module.__file__} does not define main()"
|
|
181
|
+
raise TypeError(message)
|
|
182
|
+
return entry
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
186
|
+
from favlo.logs import install_rolling_stdio
|
|
187
|
+
|
|
188
|
+
install_rolling_stdio()
|
|
189
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
190
|
+
try:
|
|
191
|
+
names = pipeline_names()
|
|
192
|
+
except ValueError as exc:
|
|
193
|
+
sys.stderr.write(f"{exc}\n")
|
|
194
|
+
return 2
|
|
195
|
+
if not args:
|
|
196
|
+
sys.stderr.write(_help(names))
|
|
197
|
+
return 2
|
|
198
|
+
if args[0] in {"-h", "--help"}:
|
|
199
|
+
sys.stdout.write(_help(names))
|
|
200
|
+
return 0
|
|
201
|
+
if args[0] == "serve":
|
|
202
|
+
from favlo.listener import serve_main
|
|
203
|
+
|
|
204
|
+
return int(serve_main(args[1:]))
|
|
205
|
+
if args[0] == "web":
|
|
206
|
+
from favlo.web_host import main as web_main
|
|
207
|
+
|
|
208
|
+
return int(web_main(args[1:]))
|
|
209
|
+
flag = args[0]
|
|
210
|
+
if not flag.startswith("--") or flag == "--":
|
|
211
|
+
sys.stderr.write(_help(names))
|
|
212
|
+
sys.stderr.write(f"\nunknown argument: {flag}\n")
|
|
213
|
+
return 2
|
|
214
|
+
name = flag[2:]
|
|
215
|
+
if name not in names:
|
|
216
|
+
known = ", ".join(f"--{item}" for item in names)
|
|
217
|
+
sys.stderr.write(f"unknown model flag: {flag}\nknown: {known}\n")
|
|
218
|
+
return 2
|
|
219
|
+
try:
|
|
220
|
+
module = load_pipeline(name)
|
|
221
|
+
entry = pipeline_main(module)
|
|
222
|
+
except (FileNotFoundError, ImportError, TypeError) as exc:
|
|
223
|
+
sys.stderr.write(f"{exc}\n")
|
|
224
|
+
return 2
|
|
225
|
+
return int(entry(args[1:]))
|
favlo/config_tools.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""The configuration tool group. Favlo web enables it on the Favlo agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.request
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
GROUP_ID = "favlo-config"
|
|
10
|
+
GROUP_NAME = "Configuration tools"
|
|
11
|
+
|
|
12
|
+
INSTRUCTIONS = (
|
|
13
|
+
"When someone asks how to connect a client, which address or model id to use, "
|
|
14
|
+
"how to download or quantize a supported model, or how to register one, "
|
|
15
|
+
"call config_read_guide before you answer. "
|
|
16
|
+
"Use clients for the server address, the model field, and reload. "
|
|
17
|
+
"Ctrl+C stops a terminal favlo serve. "
|
|
18
|
+
"Use models for which checkpoints can be downloaded and how registration works. "
|
|
19
|
+
"When someone wants to set up Qwen3.6-35B-A3B, call config_read_setup with qwen36-35b-a3b "
|
|
20
|
+
"before you answer. When someone wants to set up Qwen3.8-27B, call config_read_setup with "
|
|
21
|
+
"qwen38-27b before you answer. When someone wants to set up Z-Image-Turbo, call "
|
|
22
|
+
"config_read_setup with z-image-turbo before you answer. When someone wants to set up "
|
|
23
|
+
"Wan 2.2 I2V, call config_read_setup with wan22-i2v before you answer. When someone wants to "
|
|
24
|
+
"set up ACE-Step, call config_read_setup with ace-step-15-turbo before you answer. For any other "
|
|
25
|
+
"pipeline "
|
|
26
|
+
"they want to set up, download, quantize, or delete, call config_read_setup with that pipeline id. "
|
|
27
|
+
"Answer from that guide. "
|
|
28
|
+
"When they want you to run a step from it, call config_run_setup with that command. "
|
|
29
|
+
"config_run_setup only runs inside this install. Do not run a command the guide does not contain. "
|
|
30
|
+
"When someone asks which models are registered, or which model id to send, "
|
|
31
|
+
"call config_list_models before you answer. Report only the lines it returns. "
|
|
32
|
+
"When someone wants MTP on or off for a model, or a different draft depth, "
|
|
33
|
+
"call config_set_model_mtp. A row that omits MTP stays off. "
|
|
34
|
+
"When someone asks where your name came from, or who Amanda and Celestia are, "
|
|
35
|
+
"call config_read_guide with history and tell that story. Do not add events it does not contain. "
|
|
36
|
+
"Answer from that guide. If the guide does not say it, say you do not know."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def tools(base: str, home: Path | None) -> list[object]:
|
|
41
|
+
"""The configuration tools, in the order the group offers them."""
|
|
42
|
+
return [
|
|
43
|
+
_guide_tool(),
|
|
44
|
+
_models_tool(),
|
|
45
|
+
_mtp_tool(),
|
|
46
|
+
_suggestions_tool(home),
|
|
47
|
+
_setup_guide_tool(),
|
|
48
|
+
_setup_run_tool(base),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def registered_models(root: Path | None = None) -> str:
|
|
53
|
+
"""The registry listing. An empty registry says so."""
|
|
54
|
+
from favlo.registry import format_listing
|
|
55
|
+
|
|
56
|
+
listing = format_listing(root).strip()
|
|
57
|
+
if not listing:
|
|
58
|
+
return "no models are registered"
|
|
59
|
+
return listing
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _guide_tool() -> object:
|
|
63
|
+
from typing import Annotated
|
|
64
|
+
|
|
65
|
+
from agent_framework import tool
|
|
66
|
+
from pydantic import Field
|
|
67
|
+
|
|
68
|
+
from favlo.guides import read_guide as load_guide
|
|
69
|
+
|
|
70
|
+
@tool(approval_mode="never_require", name="config_read_guide")
|
|
71
|
+
def read_guide(
|
|
72
|
+
name: Annotated[str, Field(description="clients, models, or history.")],
|
|
73
|
+
) -> str:
|
|
74
|
+
"""Read one guide.
|
|
75
|
+
|
|
76
|
+
clients points a program at this server and covers ``reload``.
|
|
77
|
+
Ctrl+C stops a terminal ``favlo serve``. models covers
|
|
78
|
+
download, quantize, and registration. history is where the name Favlo came from.
|
|
79
|
+
"""
|
|
80
|
+
return load_guide(name)
|
|
81
|
+
|
|
82
|
+
return read_guide
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _suggestions_tool(home: Path | None) -> object:
|
|
86
|
+
from typing import Annotated
|
|
87
|
+
|
|
88
|
+
from agent_framework import tool
|
|
89
|
+
from pydantic import Field
|
|
90
|
+
|
|
91
|
+
from favlo.machine import set_suggestions
|
|
92
|
+
|
|
93
|
+
@tool(approval_mode="never_require", name="config_download_suggestions")
|
|
94
|
+
def download_suggestions(
|
|
95
|
+
enabled: Annotated[
|
|
96
|
+
bool,
|
|
97
|
+
Field(description="False stores download reminders as off. True stores them as on."),
|
|
98
|
+
],
|
|
99
|
+
) -> str:
|
|
100
|
+
"""Store whether download reminders are enabled.
|
|
101
|
+
|
|
102
|
+
The terminal does not start a reminder on its own. This records the preference.
|
|
103
|
+
"""
|
|
104
|
+
return set_suggestions(enabled, home)
|
|
105
|
+
|
|
106
|
+
return download_suggestions
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _setup_guide_tool() -> object:
|
|
110
|
+
from typing import Annotated
|
|
111
|
+
|
|
112
|
+
from agent_framework import tool
|
|
113
|
+
from pydantic import Field
|
|
114
|
+
|
|
115
|
+
from favlo.guides import read_setup as load_setup
|
|
116
|
+
|
|
117
|
+
@tool(approval_mode="never_require", name="config_read_setup")
|
|
118
|
+
def read_setup(
|
|
119
|
+
pipeline: Annotated[
|
|
120
|
+
str,
|
|
121
|
+
Field(description="Pipeline id, such as wan22-i2v."),
|
|
122
|
+
],
|
|
123
|
+
) -> str:
|
|
124
|
+
"""Read one pipeline's setup guide.
|
|
125
|
+
|
|
126
|
+
Call this when someone wants to set up, download, quantize, or delete a model.
|
|
127
|
+
qwen36-35b-a3b is Qwen3.6-35B-A3B. qwen38-27b is Qwen3.8-27B.
|
|
128
|
+
z-image-turbo is Z-Image-Turbo. wan22-i2v is Wan 2.2 I2V.
|
|
129
|
+
ace-step-15-turbo is ACE-Step.
|
|
130
|
+
The result is that pipeline's setup page.
|
|
131
|
+
A pipeline with no setup page names the ones that have one.
|
|
132
|
+
"""
|
|
133
|
+
return load_setup(pipeline)
|
|
134
|
+
|
|
135
|
+
return read_setup
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _setup_run_tool(base: str) -> object:
|
|
139
|
+
from typing import Annotated
|
|
140
|
+
|
|
141
|
+
from agent_framework import tool
|
|
142
|
+
from pydantic import Field
|
|
143
|
+
|
|
144
|
+
from favlo.setup_run import accepted_argv, frees_model, run_setup_command
|
|
145
|
+
|
|
146
|
+
@tool(approval_mode="never_require", name="config_run_setup")
|
|
147
|
+
def run_setup(
|
|
148
|
+
command: Annotated[
|
|
149
|
+
str,
|
|
150
|
+
Field(description="One command copied from the setup guide."),
|
|
151
|
+
],
|
|
152
|
+
) -> str:
|
|
153
|
+
"""Run one setup-guide command in this install.
|
|
154
|
+
|
|
155
|
+
The working directory is the folder where Favlo is installed. A path cannot
|
|
156
|
+
leave that folder. This is not a shell: pipes, cd, and other programs are refused.
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
argv = accepted_argv(command, _install())
|
|
160
|
+
except ValueError as exc:
|
|
161
|
+
return str(exc)
|
|
162
|
+
if frees_model(argv):
|
|
163
|
+
closed = _close_resident(base)
|
|
164
|
+
if closed is not None:
|
|
165
|
+
return closed
|
|
166
|
+
try:
|
|
167
|
+
return run_setup_command(command)
|
|
168
|
+
except ValueError as exc:
|
|
169
|
+
return str(exc)
|
|
170
|
+
|
|
171
|
+
return run_setup
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _install() -> Path:
|
|
175
|
+
from favlo.cli import repo_root
|
|
176
|
+
|
|
177
|
+
return repo_root()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _close_resident(base: str) -> str | None:
|
|
181
|
+
request = urllib.request.Request(base + "/pipelines/close", data=b"", method="POST")
|
|
182
|
+
try:
|
|
183
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
184
|
+
response.read()
|
|
185
|
+
except (OSError, urllib.error.URLError) as exc:
|
|
186
|
+
return f"could not close the open model: {exc}"
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _models_tool() -> object:
|
|
191
|
+
from agent_framework import tool
|
|
192
|
+
|
|
193
|
+
@tool(approval_mode="never_require", name="config_list_models")
|
|
194
|
+
def list_models() -> str:
|
|
195
|
+
"""List the registered models.
|
|
196
|
+
|
|
197
|
+
Each line is one record: folder, pipeline, alias, key/value mode,
|
|
198
|
+
and whether MTP is on with its draft depth.
|
|
199
|
+
An unregistered complete record is named too. This is the live registry,
|
|
200
|
+
not the download guide.
|
|
201
|
+
"""
|
|
202
|
+
return registered_models()
|
|
203
|
+
|
|
204
|
+
return list_models
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _mtp_tool() -> object:
|
|
208
|
+
from typing import Annotated
|
|
209
|
+
|
|
210
|
+
from agent_framework import tool
|
|
211
|
+
from pydantic import Field
|
|
212
|
+
|
|
213
|
+
from favlo.registry import set_model_mtp as store_model_mtp
|
|
214
|
+
|
|
215
|
+
@tool(approval_mode="never_require", name="config_set_model_mtp")
|
|
216
|
+
def set_model_mtp(
|
|
217
|
+
model: Annotated[str, Field(description="Folder, alias, or pipeline id.")],
|
|
218
|
+
enabled: Annotated[bool, Field(description="True drafts. False stays on one token.")],
|
|
219
|
+
depth: Annotated[
|
|
220
|
+
int | None,
|
|
221
|
+
Field(description="Draft length. Omit it to keep the depth already stored."),
|
|
222
|
+
] = None,
|
|
223
|
+
) -> str:
|
|
224
|
+
"""Store MTP and its draft depth on one registry row.
|
|
225
|
+
|
|
226
|
+
The next completion for that model uses the row when the request omits
|
|
227
|
+
``mtp`` or ``mtp_depth``. A request field still wins.
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
return store_model_mtp(model, enabled=enabled, depth=depth)
|
|
231
|
+
except (LookupError, ValueError) as exc:
|
|
232
|
+
return str(exc)
|
|
233
|
+
|
|
234
|
+
return set_model_mtp
|