flare-kernel 0.1.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.
- flare_kernel/__init__.py +9 -0
- flare_kernel/app.py +27 -0
- flare_kernel/contracts/__init__.py +5 -0
- flare_kernel/contracts/kernel_contract.py +36 -0
- flare_kernel/main.py +5 -0
- flare_kernel/router/__init__.py +5 -0
- flare_kernel/router/kernel.py +342 -0
- flare_kernel/runtime/__init__.py +26 -0
- flare_kernel/runtime/agent_runtime.py +113 -0
- flare_kernel/runtime/domain_pack_loader.py +240 -0
- flare_kernel/runtime/llm_provider.py +204 -0
- flare_kernel/runtime/logging.py +21 -0
- flare_kernel/runtime/mode_orchestration.py +330 -0
- flare_kernel/runtime/mode_runtime.py +262 -0
- flare_kernel/runtime/skill_runtime.py +105 -0
- flare_kernel/runtime/tests/test_kernel_runtime.py +114 -0
- flare_kernel/runtime/trace.py +11 -0
- flare_kernel-0.1.0.dist-info/METADATA +67 -0
- flare_kernel-0.1.0.dist-info/RECORD +21 -0
- flare_kernel-0.1.0.dist-info/WHEEL +5 -0
- flare_kernel-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Helpers for loading instance-scoped domain-pack assets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from flare_kernel.runtime.logging import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger("flare_kernel.runtime.domain_pack_loader")
|
|
14
|
+
|
|
15
|
+
_DOMAIN_PACK_ROOT_ENV = "FLARE_DOMAIN_PACK_ROOT"
|
|
16
|
+
_INSTANCE_PROFILE_FILE = "branding/instance-branding.json"
|
|
17
|
+
_REQUIREMENT_SCHEMA_GLOBS = ("schema/*.yaml", "schema/*.yml")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _resolve_from_env() -> Path | None:
|
|
21
|
+
env_path = os.getenv(_DOMAIN_PACK_ROOT_ENV)
|
|
22
|
+
if not env_path:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
root = Path(env_path).expanduser().resolve()
|
|
26
|
+
if not root.exists():
|
|
27
|
+
logger.warning("domain_pack.root_not_found env=%s path=%s", _DOMAIN_PACK_ROOT_ENV, str(root))
|
|
28
|
+
return None
|
|
29
|
+
return root
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _resolve_from_repo(instance_id: str) -> Path | None:
|
|
33
|
+
current_file = Path(__file__).resolve()
|
|
34
|
+
for parent in current_file.parents:
|
|
35
|
+
candidate = parent / "apps" / instance_id / "domain-pack"
|
|
36
|
+
if candidate.exists():
|
|
37
|
+
return candidate
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_domain_pack_root(instance_id: str) -> Path | None:
|
|
42
|
+
env_root = _resolve_from_env()
|
|
43
|
+
if env_root is not None:
|
|
44
|
+
return env_root
|
|
45
|
+
return _resolve_from_repo(instance_id)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _parse_scalar(raw_value: str) -> Any:
|
|
49
|
+
value = raw_value.strip()
|
|
50
|
+
if not value:
|
|
51
|
+
return ""
|
|
52
|
+
|
|
53
|
+
if value.startswith(("'", '"')) and value.endswith(("'", '"')) and len(value) >= 2:
|
|
54
|
+
return value[1:-1]
|
|
55
|
+
|
|
56
|
+
lowered = value.lower()
|
|
57
|
+
if lowered == "true":
|
|
58
|
+
return True
|
|
59
|
+
if lowered == "false":
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
return int(value)
|
|
64
|
+
except ValueError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
return float(value)
|
|
69
|
+
except ValueError:
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _load_requirement_schema_text(text: str, *, source_path: Path) -> dict[str, Any]:
|
|
74
|
+
lines = text.splitlines()
|
|
75
|
+
parsed: dict[str, Any] = {}
|
|
76
|
+
index = 0
|
|
77
|
+
|
|
78
|
+
while index < len(lines):
|
|
79
|
+
raw_line = lines[index].rstrip()
|
|
80
|
+
stripped = raw_line.strip()
|
|
81
|
+
if not stripped or stripped.startswith("#"):
|
|
82
|
+
index += 1
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
indent = len(raw_line) - len(raw_line.lstrip(" "))
|
|
86
|
+
if indent != 0 or ":" not in stripped:
|
|
87
|
+
index += 1
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
key, raw_value = stripped.split(":", 1)
|
|
91
|
+
key = key.strip()
|
|
92
|
+
value = raw_value.strip()
|
|
93
|
+
|
|
94
|
+
if key == "required_fields" and not value:
|
|
95
|
+
index += 1
|
|
96
|
+
required_fields: list[dict[str, Any]] = []
|
|
97
|
+
while index < len(lines):
|
|
98
|
+
item_line = lines[index].rstrip()
|
|
99
|
+
item_stripped = item_line.strip()
|
|
100
|
+
if not item_stripped or item_stripped.startswith("#"):
|
|
101
|
+
index += 1
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
item_indent = len(item_line) - len(item_line.lstrip(" "))
|
|
105
|
+
if item_indent < 2:
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
if item_indent != 2 or not item_stripped.startswith("- "):
|
|
109
|
+
index += 1
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
field_item: dict[str, Any] = {}
|
|
113
|
+
first_field = item_stripped[2:].strip()
|
|
114
|
+
if first_field and ":" in first_field:
|
|
115
|
+
item_key, item_value = first_field.split(":", 1)
|
|
116
|
+
field_item[item_key.strip()] = _parse_scalar(item_value)
|
|
117
|
+
|
|
118
|
+
index += 1
|
|
119
|
+
while index < len(lines):
|
|
120
|
+
field_line = lines[index].rstrip()
|
|
121
|
+
field_stripped = field_line.strip()
|
|
122
|
+
if not field_stripped or field_stripped.startswith("#"):
|
|
123
|
+
index += 1
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
field_indent = len(field_line) - len(field_line.lstrip(" "))
|
|
127
|
+
if field_indent <= 2:
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
if ":" in field_stripped:
|
|
131
|
+
item_key, item_value = field_stripped.split(":", 1)
|
|
132
|
+
field_item[item_key.strip()] = _parse_scalar(item_value)
|
|
133
|
+
index += 1
|
|
134
|
+
|
|
135
|
+
required_fields.append(field_item)
|
|
136
|
+
|
|
137
|
+
parsed[key] = required_fields
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if key == "ask_strategy" and not value:
|
|
141
|
+
index += 1
|
|
142
|
+
ask_strategy: dict[str, Any] = {}
|
|
143
|
+
while index < len(lines):
|
|
144
|
+
strategy_line = lines[index].rstrip()
|
|
145
|
+
strategy_stripped = strategy_line.strip()
|
|
146
|
+
if not strategy_stripped or strategy_stripped.startswith("#"):
|
|
147
|
+
index += 1
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
strategy_indent = len(strategy_line) - len(strategy_line.lstrip(" "))
|
|
151
|
+
if strategy_indent < 2:
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
if ":" in strategy_stripped and not strategy_stripped.startswith("- "):
|
|
155
|
+
strategy_key, strategy_value = strategy_stripped.split(":", 1)
|
|
156
|
+
ask_strategy[strategy_key.strip()] = _parse_scalar(strategy_value)
|
|
157
|
+
index += 1
|
|
158
|
+
|
|
159
|
+
parsed[key] = ask_strategy
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
if value:
|
|
163
|
+
parsed[key] = _parse_scalar(value)
|
|
164
|
+
index += 1
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
index += 1
|
|
168
|
+
while index < len(lines):
|
|
169
|
+
nested_line = lines[index].rstrip()
|
|
170
|
+
nested_stripped = nested_line.strip()
|
|
171
|
+
if not nested_stripped or nested_stripped.startswith("#"):
|
|
172
|
+
index += 1
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
nested_indent = len(nested_line) - len(nested_line.lstrip(" "))
|
|
176
|
+
if nested_indent < 2:
|
|
177
|
+
break
|
|
178
|
+
index += 1
|
|
179
|
+
|
|
180
|
+
parsed["schema_path"] = str(source_path)
|
|
181
|
+
return parsed
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _find_requirement_schema_path(root: Path) -> Path | None:
|
|
185
|
+
for pattern in _REQUIREMENT_SCHEMA_GLOBS:
|
|
186
|
+
for candidate in root.glob(pattern):
|
|
187
|
+
if candidate.is_file():
|
|
188
|
+
return candidate
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@lru_cache(maxsize=8)
|
|
193
|
+
def load_requirement_schema(instance_id: str) -> dict[str, Any] | None:
|
|
194
|
+
root = resolve_domain_pack_root(instance_id)
|
|
195
|
+
if root is None:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
schema_path = _find_requirement_schema_path(root)
|
|
199
|
+
if schema_path is None:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
schema_text = schema_path.read_text(encoding="utf-8")
|
|
204
|
+
except OSError:
|
|
205
|
+
logger.warning("domain_pack.read_failed file=%s", str(schema_path))
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
schema = _load_requirement_schema_text(schema_text, source_path=schema_path)
|
|
210
|
+
except Exception as exc:
|
|
211
|
+
logger.warning("domain_pack.schema_parse_failed file=%s error=%s", str(schema_path), str(exc))
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
schema["root"] = str(root)
|
|
215
|
+
return schema
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@lru_cache(maxsize=8)
|
|
219
|
+
def load_instance_profile(instance_id: str) -> dict[str, Any] | None:
|
|
220
|
+
root = resolve_domain_pack_root(instance_id)
|
|
221
|
+
if root is None:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
profile_path = root / _INSTANCE_PROFILE_FILE
|
|
225
|
+
if not profile_path.exists():
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
data = json.loads(profile_path.read_text(encoding="utf-8"))
|
|
230
|
+
except json.JSONDecodeError:
|
|
231
|
+
logger.warning("domain_pack.invalid_json file=%s", str(profile_path))
|
|
232
|
+
return None
|
|
233
|
+
except OSError:
|
|
234
|
+
logger.warning("domain_pack.read_failed file=%s", str(profile_path))
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
if not isinstance(data, dict):
|
|
238
|
+
logger.warning("domain_pack.invalid_profile file=%s", str(profile_path))
|
|
239
|
+
return None
|
|
240
|
+
return data
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""LLM provider adapter layer for the kernel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from flare_kernel.runtime.logging import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger("flare_kernel.runtime.llm_provider")
|
|
14
|
+
|
|
15
|
+
_DEFAULT_PROVIDER_BACKEND = "dashscope"
|
|
16
|
+
_DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
17
|
+
_DASHSCOPE_MODEL = "qwen-plus"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class LLMProviderConfig:
|
|
22
|
+
backend: str
|
|
23
|
+
dashscope_api_key: str | None
|
|
24
|
+
dashscope_base_url: str
|
|
25
|
+
dashscope_model: str
|
|
26
|
+
timeout_seconds: float
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class LLMCompletion:
|
|
31
|
+
provider: str
|
|
32
|
+
model: str
|
|
33
|
+
content: str
|
|
34
|
+
status: str
|
|
35
|
+
details: dict[str, Any]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _env(name: str, default: str | None = None) -> str | None:
|
|
39
|
+
raw = os.getenv(name)
|
|
40
|
+
if raw is None:
|
|
41
|
+
return default
|
|
42
|
+
value = raw.strip()
|
|
43
|
+
return value if value else default
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _placeholder_content(provider: str, model: str, reason: str | None = None) -> str:
|
|
47
|
+
suffix = f" ({reason})" if reason else ""
|
|
48
|
+
return f"[{provider}:{model}] LLM placeholder{suffix}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _float_env(name: str, default: float) -> float:
|
|
52
|
+
raw = _env(name)
|
|
53
|
+
if raw is None:
|
|
54
|
+
return default
|
|
55
|
+
try:
|
|
56
|
+
return float(raw)
|
|
57
|
+
except ValueError:
|
|
58
|
+
logger.warning("llm.provider.invalid_timeout env=%s raw=%s", name, raw)
|
|
59
|
+
return default
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@lru_cache(maxsize=1)
|
|
63
|
+
def get_llm_provider_config() -> LLMProviderConfig:
|
|
64
|
+
backend = (_env("LLM_PROVIDER_BACKEND", _DEFAULT_PROVIDER_BACKEND) or _DEFAULT_PROVIDER_BACKEND).lower()
|
|
65
|
+
return LLMProviderConfig(
|
|
66
|
+
backend=backend,
|
|
67
|
+
dashscope_api_key=_env("DASHSCOPE_API_KEY"),
|
|
68
|
+
dashscope_base_url=_env("DASHSCOPE_BASE_URL", _DASHSCOPE_BASE_URL) or _DASHSCOPE_BASE_URL,
|
|
69
|
+
dashscope_model=_env("DASHSCOPE_MODEL", _DASHSCOPE_MODEL) or _DASHSCOPE_MODEL,
|
|
70
|
+
timeout_seconds=_float_env("LLM_PROVIDER_TIMEOUT_SECONDS", 60.0),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def generate_llm_completion(prompt: str, *, trace_id: str, session_id: str, intent: str) -> LLMCompletion:
|
|
75
|
+
config = get_llm_provider_config()
|
|
76
|
+
|
|
77
|
+
if config.backend != "dashscope":
|
|
78
|
+
provider = config.backend
|
|
79
|
+
model = config.dashscope_model
|
|
80
|
+
logger.info("llm.provider.placeholder backend=%s trace_id=%s session_id=%s intent=%s", provider, trace_id, session_id, intent)
|
|
81
|
+
return LLMCompletion(
|
|
82
|
+
provider=provider,
|
|
83
|
+
model=model,
|
|
84
|
+
content=_placeholder_content(provider, model, "provider backend is not dashscope"),
|
|
85
|
+
status="placeholder",
|
|
86
|
+
details={"backend": provider, "reason": "provider backend is not dashscope"},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if not config.dashscope_api_key:
|
|
90
|
+
logger.info("llm.provider.missing_key backend=dashscope trace_id=%s session_id=%s intent=%s", trace_id, session_id, intent)
|
|
91
|
+
return LLMCompletion(
|
|
92
|
+
provider="dashscope",
|
|
93
|
+
model=config.dashscope_model,
|
|
94
|
+
content=_placeholder_content("dashscope", config.dashscope_model, "DASHSCOPE_API_KEY is missing"),
|
|
95
|
+
status="placeholder",
|
|
96
|
+
details={"backend": "dashscope", "reason": "DASHSCOPE_API_KEY is missing"},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
payload = {
|
|
100
|
+
"model": config.dashscope_model,
|
|
101
|
+
"messages": [
|
|
102
|
+
{
|
|
103
|
+
"role": "system",
|
|
104
|
+
"content": "You are FLARE kernel. Respond concisely and helpfully.",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"role": "user",
|
|
108
|
+
"content": prompt,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
"temperature": 0.2,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
headers = {
|
|
115
|
+
"Authorization": f"Bearer {config.dashscope_api_key}",
|
|
116
|
+
"Content-Type": "application/json",
|
|
117
|
+
}
|
|
118
|
+
url = f"{config.dashscope_base_url.rstrip('/')}/chat/completions"
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
import httpx
|
|
122
|
+
except ImportError:
|
|
123
|
+
logger.warning("llm.provider.httpx_missing backend=dashscope trace_id=%s session_id=%s intent=%s", trace_id, session_id, intent)
|
|
124
|
+
return LLMCompletion(
|
|
125
|
+
provider="dashscope",
|
|
126
|
+
model=config.dashscope_model,
|
|
127
|
+
content=_placeholder_content("dashscope", config.dashscope_model, "httpx is not installed"),
|
|
128
|
+
status="placeholder",
|
|
129
|
+
details={"backend": "dashscope", "reason": "httpx_missing"},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
async with httpx.AsyncClient(timeout=config.timeout_seconds) as client:
|
|
134
|
+
response = await client.post(url, json=payload, headers=headers)
|
|
135
|
+
except httpx.HTTPError as exc:
|
|
136
|
+
logger.warning("llm.provider.request_failed backend=dashscope trace_id=%s session_id=%s intent=%s error=%s", trace_id, session_id, intent, str(exc))
|
|
137
|
+
return LLMCompletion(
|
|
138
|
+
provider="dashscope",
|
|
139
|
+
model=config.dashscope_model,
|
|
140
|
+
content=_placeholder_content("dashscope", config.dashscope_model, "request failed"),
|
|
141
|
+
status="placeholder",
|
|
142
|
+
details={"backend": "dashscope", "reason": "request failed", "error": str(exc)},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if response.status_code != 200:
|
|
146
|
+
logger.warning(
|
|
147
|
+
"llm.provider.http_error backend=dashscope trace_id=%s session_id=%s intent=%s status=%s",
|
|
148
|
+
trace_id,
|
|
149
|
+
session_id,
|
|
150
|
+
intent,
|
|
151
|
+
response.status_code,
|
|
152
|
+
)
|
|
153
|
+
return LLMCompletion(
|
|
154
|
+
provider="dashscope",
|
|
155
|
+
model=config.dashscope_model,
|
|
156
|
+
content=_placeholder_content("dashscope", config.dashscope_model, f"HTTP {response.status_code}"),
|
|
157
|
+
status="placeholder",
|
|
158
|
+
details={
|
|
159
|
+
"backend": "dashscope",
|
|
160
|
+
"reason": "http_error",
|
|
161
|
+
"status_code": response.status_code,
|
|
162
|
+
"response_text": response.text[:512],
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
data = response.json()
|
|
168
|
+
except json.JSONDecodeError:
|
|
169
|
+
logger.warning("llm.provider.invalid_json backend=dashscope trace_id=%s session_id=%s intent=%s", trace_id, session_id, intent)
|
|
170
|
+
return LLMCompletion(
|
|
171
|
+
provider="dashscope",
|
|
172
|
+
model=config.dashscope_model,
|
|
173
|
+
content=_placeholder_content("dashscope", config.dashscope_model, "invalid JSON response"),
|
|
174
|
+
status="placeholder",
|
|
175
|
+
details={"backend": "dashscope", "reason": "invalid_json", "response_text": response.text[:512]},
|
|
176
|
+
)
|
|
177
|
+
choices = data.get("choices") if isinstance(data, dict) else None
|
|
178
|
+
message = ""
|
|
179
|
+
if isinstance(choices, list) and choices:
|
|
180
|
+
first_choice = choices[0]
|
|
181
|
+
if isinstance(first_choice, dict):
|
|
182
|
+
choice_message = first_choice.get("message")
|
|
183
|
+
if isinstance(choice_message, dict):
|
|
184
|
+
content = choice_message.get("content")
|
|
185
|
+
if isinstance(content, str):
|
|
186
|
+
message = content.strip()
|
|
187
|
+
|
|
188
|
+
if not message:
|
|
189
|
+
logger.warning("llm.provider.empty_response backend=dashscope trace_id=%s session_id=%s intent=%s", trace_id, session_id, intent)
|
|
190
|
+
return LLMCompletion(
|
|
191
|
+
provider="dashscope",
|
|
192
|
+
model=config.dashscope_model,
|
|
193
|
+
content=_placeholder_content("dashscope", config.dashscope_model, "empty response"),
|
|
194
|
+
status="placeholder",
|
|
195
|
+
details={"backend": "dashscope", "reason": "empty response", "response": data},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return LLMCompletion(
|
|
199
|
+
provider="dashscope",
|
|
200
|
+
model=config.dashscope_model,
|
|
201
|
+
content=message,
|
|
202
|
+
status="ok",
|
|
203
|
+
details={"backend": "dashscope"},
|
|
204
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Kernel logging setup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
_LOG_FORMAT = "%(asctime)s %(levelname)s %(name)s %(message)s"
|
|
8
|
+
_configured = False
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _ensure_basic_logging() -> None:
|
|
12
|
+
global _configured
|
|
13
|
+
if _configured:
|
|
14
|
+
return
|
|
15
|
+
logging.basicConfig(level=logging.INFO, format=_LOG_FORMAT)
|
|
16
|
+
_configured = True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_logger(name: str) -> logging.Logger:
|
|
20
|
+
_ensure_basic_logging()
|
|
21
|
+
return logging.getLogger(name)
|