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.
@@ -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)