friday-framework-runtime 0.1.0a0__tar.gz → 0.1.0a2__tar.gz
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.
- {friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/PKG-INFO +9 -8
- {friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/pyproject.toml +9 -7
- {friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/src/friday_runtime/config.py +200 -24
- {friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/src/friday_runtime/di.py +60 -27
- friday_framework_runtime-0.1.0a2/src/friday_runtime/model_library.py +172 -0
- friday_framework_runtime-0.1.0a2/tests_runtime/test_model_library.py +192 -0
- {friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/.gitignore +0 -0
- {friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/README.md +0 -0
- {friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/src/friday_runtime/__init__.py +0 -0
- {friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/src/friday_runtime/kernel.py +0 -0
- {friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/src/friday_runtime/tool_loading.py +0 -0
- {friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/tests_runtime/__init__.py +0 -0
- {friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/tests_runtime/test_config.py +0 -0
- {friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/tests_runtime/test_di.py +0 -0
- {friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/tests_runtime/test_kernel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: friday-framework-runtime
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a2
|
|
4
4
|
Summary: Headless runtime kernel for the Friday Agent Framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/CIChuck/agent-framework
|
|
6
6
|
Project-URL: Repository, https://github.com/CIChuck/agent-framework
|
|
@@ -10,12 +10,13 @@ Project-URL: Source, https://github.com/CIChuck/agent-framework/tree/main/packag
|
|
|
10
10
|
Author: Friday Team
|
|
11
11
|
License-Expression: MIT
|
|
12
12
|
Requires-Python: >=3.10
|
|
13
|
-
Requires-Dist: friday-framework-agent==0.1.
|
|
14
|
-
Requires-Dist: friday-framework-core==0.1.
|
|
15
|
-
Requires-Dist: friday-framework-llm==0.1.
|
|
16
|
-
Requires-Dist: friday-framework-
|
|
17
|
-
Requires-Dist: friday-framework-
|
|
18
|
-
Requires-Dist: friday-framework-
|
|
13
|
+
Requires-Dist: friday-framework-agent==0.1.0a2
|
|
14
|
+
Requires-Dist: friday-framework-core==0.1.0a2
|
|
15
|
+
Requires-Dist: friday-framework-llm==0.1.0a2
|
|
16
|
+
Requires-Dist: friday-framework-mcp==0.1.0a2
|
|
17
|
+
Requires-Dist: friday-framework-memory==0.1.0a2
|
|
18
|
+
Requires-Dist: friday-framework-telemetry==0.1.0a2
|
|
19
|
+
Requires-Dist: friday-framework-transcript==0.1.0a2
|
|
19
20
|
Provides-Extra: dev
|
|
20
21
|
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
21
22
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "friday-framework-runtime"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.0a2"
|
|
4
4
|
description = "Headless runtime kernel for the Friday Agent Framework"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
license = "MIT"
|
|
8
8
|
authors = [{ name = "Friday Team" }]
|
|
9
9
|
dependencies = [
|
|
10
|
-
"friday-framework-core==0.1.
|
|
11
|
-
"friday-framework-memory==0.1.
|
|
12
|
-
"friday-framework-llm==0.1.
|
|
13
|
-
"friday-framework-telemetry==0.1.
|
|
14
|
-
"friday-framework-agent==0.1.
|
|
15
|
-
"friday-framework-transcript==0.1.
|
|
10
|
+
"friday-framework-core==0.1.0a2",
|
|
11
|
+
"friday-framework-memory==0.1.0a2",
|
|
12
|
+
"friday-framework-llm==0.1.0a2",
|
|
13
|
+
"friday-framework-telemetry==0.1.0a2",
|
|
14
|
+
"friday-framework-agent==0.1.0a2",
|
|
15
|
+
"friday-framework-transcript==0.1.0a2",
|
|
16
|
+
"friday-framework-mcp==0.1.0a2",
|
|
16
17
|
]
|
|
17
18
|
|
|
18
19
|
[project.urls]
|
|
@@ -47,3 +48,4 @@ friday-framework-llm = { workspace = true }
|
|
|
47
48
|
friday-framework-telemetry = { workspace = true }
|
|
48
49
|
friday-framework-agent = { workspace = true }
|
|
49
50
|
friday-framework-transcript = { workspace = true }
|
|
51
|
+
friday-framework-mcp = { workspace = true }
|
{friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/src/friday_runtime/config.py
RENAMED
|
@@ -14,9 +14,20 @@ from dataclasses import dataclass, field
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Any, Literal, cast
|
|
16
16
|
|
|
17
|
-
import yaml
|
|
17
|
+
import yaml
|
|
18
|
+
from friday_llm import (
|
|
19
|
+
ChatServiceBinding,
|
|
20
|
+
EmbeddingServiceBinding,
|
|
21
|
+
EmbeddingServiceSpec,
|
|
22
|
+
LLMServiceSpec,
|
|
23
|
+
ModelLibrary,
|
|
24
|
+
resolve_chat_service,
|
|
25
|
+
resolve_embedding_service,
|
|
26
|
+
)
|
|
18
27
|
from friday_mcp.config import MCPConfig
|
|
19
28
|
|
|
29
|
+
from friday_runtime.model_library import load_model_library
|
|
30
|
+
|
|
20
31
|
|
|
21
32
|
def _coerce_bool(value: str) -> bool:
|
|
22
33
|
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
@@ -265,7 +276,7 @@ class LLMServiceConfig:
|
|
|
265
276
|
@dataclass(frozen=True)
|
|
266
277
|
class LLMPoolConfig:
|
|
267
278
|
default_service: str
|
|
268
|
-
services: dict[str, LLMServiceConfig]
|
|
279
|
+
services: dict[str, LLMServiceConfig | ChatServiceBinding]
|
|
269
280
|
|
|
270
281
|
|
|
271
282
|
@dataclass(frozen=True)
|
|
@@ -307,7 +318,7 @@ class EmbeddingServiceConfig:
|
|
|
307
318
|
@dataclass(frozen=True)
|
|
308
319
|
class EmbeddingPoolConfig:
|
|
309
320
|
default_service: str
|
|
310
|
-
services: dict[str, EmbeddingServiceConfig]
|
|
321
|
+
services: dict[str, EmbeddingServiceConfig | EmbeddingServiceBinding]
|
|
311
322
|
|
|
312
323
|
|
|
313
324
|
@dataclass(frozen=True)
|
|
@@ -373,15 +384,26 @@ class RuntimeConfig:
|
|
|
373
384
|
llm_api_key: str | None = None
|
|
374
385
|
llm_api_base: str | None = None
|
|
375
386
|
llm_pool_default_service: str = "default"
|
|
376
|
-
llm_pool_services: dict[str, LLMServiceConfig] = field(
|
|
387
|
+
llm_pool_services: dict[str, LLMServiceConfig | ChatServiceBinding] = field(
|
|
388
|
+
default_factory=dict
|
|
389
|
+
)
|
|
390
|
+
llm_service_specs: dict[str, LLMServiceSpec] = field(default_factory=dict)
|
|
377
391
|
|
|
378
392
|
# Embedding Configuration
|
|
379
393
|
embedding_model: str = "text-embedding-3-small"
|
|
380
394
|
embedding_pool_default_service: str = "default"
|
|
381
|
-
embedding_pool_services: dict[
|
|
395
|
+
embedding_pool_services: dict[
|
|
396
|
+
str,
|
|
397
|
+
EmbeddingServiceConfig | EmbeddingServiceBinding,
|
|
398
|
+
] = field(default_factory=dict)
|
|
399
|
+
embedding_service_specs: dict[str, EmbeddingServiceSpec] = field(
|
|
382
400
|
default_factory=dict
|
|
383
401
|
)
|
|
384
402
|
|
|
403
|
+
# Canonical model-library configuration
|
|
404
|
+
model_library_path: str | None = None
|
|
405
|
+
model_library: ModelLibrary | None = None
|
|
406
|
+
|
|
385
407
|
# Prompting Configuration
|
|
386
408
|
prompting_base_system_prompt: str | None = None
|
|
387
409
|
|
|
@@ -482,28 +504,54 @@ class RuntimeConfig:
|
|
|
482
504
|
|
|
483
505
|
if self.llm_pool_default_service not in self.llm_pool_services:
|
|
484
506
|
raise ValueError("llm_pool.default_service must reference a configured service")
|
|
507
|
+
for name, service in self.llm_pool_services.items():
|
|
508
|
+
if isinstance(service, ChatServiceBinding) and name not in self.llm_service_specs:
|
|
509
|
+
raise ValueError(
|
|
510
|
+
f"canonical llm service {name!r} has no resolved service spec"
|
|
511
|
+
)
|
|
485
512
|
|
|
486
513
|
self.embedding_pool_services = {
|
|
487
514
|
name: self._coerce_embedding_service_config(service)
|
|
488
515
|
for name, service in self.embedding_pool_services.items()
|
|
489
516
|
}
|
|
490
517
|
if not self.embedding_pool_services:
|
|
491
|
-
default_llm_service = self.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
518
|
+
default_llm_service = self.llm_pool_services[
|
|
519
|
+
self.llm_pool_default_service
|
|
520
|
+
]
|
|
521
|
+
if isinstance(default_llm_service, LLMServiceConfig):
|
|
522
|
+
self.embedding_pool_services = {
|
|
523
|
+
self.embedding_pool_default_service: EmbeddingServiceConfig(
|
|
524
|
+
provider=default_llm_service.provider,
|
|
525
|
+
model=self.embedding_model,
|
|
526
|
+
api_key=default_llm_service.api_key,
|
|
527
|
+
api_base=default_llm_service.api_base,
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
else:
|
|
531
|
+
raise ValueError(
|
|
532
|
+
"canonical model-library configuration requires an explicit "
|
|
533
|
+
"embedding_pool service"
|
|
498
534
|
)
|
|
499
|
-
}
|
|
500
535
|
|
|
501
536
|
if self.embedding_pool_default_service not in self.embedding_pool_services:
|
|
502
537
|
raise ValueError(
|
|
503
538
|
"embedding_pool.default_service must reference a configured service"
|
|
504
539
|
)
|
|
505
540
|
|
|
506
|
-
|
|
541
|
+
default_embedding = self.embedding_pool_services[
|
|
542
|
+
self.embedding_pool_default_service
|
|
543
|
+
]
|
|
544
|
+
if isinstance(default_embedding, EmbeddingServiceConfig):
|
|
545
|
+
self.embedding_model = default_embedding.model
|
|
546
|
+
else:
|
|
547
|
+
try:
|
|
548
|
+
self.embedding_model = self.embedding_service_specs[
|
|
549
|
+
self.embedding_pool_default_service
|
|
550
|
+
].model
|
|
551
|
+
except KeyError as error:
|
|
552
|
+
raise ValueError(
|
|
553
|
+
"canonical default embedding service has no resolved service spec"
|
|
554
|
+
) from error
|
|
507
555
|
|
|
508
556
|
if self.memory_extraction_llm_service is None:
|
|
509
557
|
self.memory_extraction_llm_service = self.llm_pool_default_service
|
|
@@ -530,20 +578,32 @@ class RuntimeConfig:
|
|
|
530
578
|
self.validate_transcript_path()
|
|
531
579
|
|
|
532
580
|
@staticmethod
|
|
533
|
-
def _coerce_service_config(
|
|
581
|
+
def _coerce_service_config(
|
|
582
|
+
value: LLMServiceConfig | ChatServiceBinding | Mapping[str, Any],
|
|
583
|
+
) -> LLMServiceConfig | ChatServiceBinding:
|
|
534
584
|
if isinstance(value, LLMServiceConfig):
|
|
535
585
|
return value
|
|
586
|
+
if isinstance(value, ChatServiceBinding):
|
|
587
|
+
return value
|
|
536
588
|
if isinstance(value, Mapping):
|
|
589
|
+
if "profile" in value:
|
|
590
|
+
return ChatServiceBinding.model_validate(value)
|
|
537
591
|
return LLMServiceConfig.from_dict(value)
|
|
538
592
|
raise ValueError("llm_pool.services entries must be objects")
|
|
539
593
|
|
|
540
594
|
@staticmethod
|
|
541
595
|
def _coerce_embedding_service_config(
|
|
542
|
-
value: EmbeddingServiceConfig
|
|
543
|
-
|
|
596
|
+
value: EmbeddingServiceConfig
|
|
597
|
+
| EmbeddingServiceBinding
|
|
598
|
+
| Mapping[str, Any],
|
|
599
|
+
) -> EmbeddingServiceConfig | EmbeddingServiceBinding:
|
|
544
600
|
if isinstance(value, EmbeddingServiceConfig):
|
|
545
601
|
return value
|
|
602
|
+
if isinstance(value, EmbeddingServiceBinding):
|
|
603
|
+
return value
|
|
546
604
|
if isinstance(value, Mapping):
|
|
605
|
+
if "profile" in value:
|
|
606
|
+
return EmbeddingServiceBinding.model_validate(value)
|
|
547
607
|
return EmbeddingServiceConfig.from_dict(value)
|
|
548
608
|
raise ValueError("embedding_pool.services entries must be objects")
|
|
549
609
|
|
|
@@ -648,9 +708,12 @@ class RuntimeConfig:
|
|
|
648
708
|
def get_llm_service_config(self, service_name: str | None = None) -> LLMServiceConfig:
|
|
649
709
|
selected = service_name or self.llm_pool_default_service
|
|
650
710
|
try:
|
|
651
|
-
|
|
711
|
+
service = self.llm_pool_services[selected]
|
|
652
712
|
except KeyError as exc:
|
|
653
713
|
raise ValueError(f"Unknown llm service: {selected}") from exc
|
|
714
|
+
if not isinstance(service, LLMServiceConfig):
|
|
715
|
+
raise ValueError(f"LLM service {selected!r} uses a canonical profile")
|
|
716
|
+
return service
|
|
654
717
|
|
|
655
718
|
def get_embedding_service_config(
|
|
656
719
|
self,
|
|
@@ -658,9 +721,14 @@ class RuntimeConfig:
|
|
|
658
721
|
) -> EmbeddingServiceConfig:
|
|
659
722
|
selected = service_name or self.embedding_pool_default_service
|
|
660
723
|
try:
|
|
661
|
-
|
|
724
|
+
service = self.embedding_pool_services[selected]
|
|
662
725
|
except KeyError as exc:
|
|
663
726
|
raise ValueError(f"Unknown embedding service: {selected}") from exc
|
|
727
|
+
if not isinstance(service, EmbeddingServiceConfig):
|
|
728
|
+
raise ValueError(
|
|
729
|
+
f"Embedding service {selected!r} uses a canonical profile"
|
|
730
|
+
)
|
|
731
|
+
return service
|
|
664
732
|
|
|
665
733
|
def validate_runtime_roots(self) -> None:
|
|
666
734
|
"""
|
|
@@ -808,6 +876,12 @@ class RuntimeConfig:
|
|
|
808
876
|
if yaml_path is not None:
|
|
809
877
|
yaml_data = cls._load_yaml(pathlib_path=Path(yaml_path))
|
|
810
878
|
cls._deep_merge(merged, yaml_data)
|
|
879
|
+
for pool_name in ("llm_pool", "embedding_pool"):
|
|
880
|
+
yaml_pool = yaml_data.get(pool_name)
|
|
881
|
+
if isinstance(yaml_pool, Mapping) and "services" in yaml_pool:
|
|
882
|
+
cast(dict[str, Any], merged[pool_name])["services"] = dict(
|
|
883
|
+
cast(Mapping[str, Any], yaml_pool["services"])
|
|
884
|
+
)
|
|
811
885
|
|
|
812
886
|
allow_env = yaml_path is None or bool(
|
|
813
887
|
merged["config_policy"]["allow_env_override"]
|
|
@@ -818,7 +892,63 @@ class RuntimeConfig:
|
|
|
818
892
|
if overrides:
|
|
819
893
|
cls._apply_flat_overrides(merged, overrides)
|
|
820
894
|
|
|
821
|
-
|
|
895
|
+
flat = cls._nested_to_flat_kwargs(merged)
|
|
896
|
+
canonical_llm = {
|
|
897
|
+
name: service
|
|
898
|
+
for name, service in cast(
|
|
899
|
+
dict[str, LLMServiceConfig | ChatServiceBinding],
|
|
900
|
+
flat["llm_pool_services"],
|
|
901
|
+
).items()
|
|
902
|
+
if isinstance(service, ChatServiceBinding)
|
|
903
|
+
}
|
|
904
|
+
canonical_embeddings = {
|
|
905
|
+
name: service
|
|
906
|
+
for name, service in cast(
|
|
907
|
+
dict[str, EmbeddingServiceConfig | EmbeddingServiceBinding],
|
|
908
|
+
flat["embedding_pool_services"],
|
|
909
|
+
).items()
|
|
910
|
+
if isinstance(service, EmbeddingServiceBinding)
|
|
911
|
+
}
|
|
912
|
+
model_library_section = cast(
|
|
913
|
+
Mapping[str, Any],
|
|
914
|
+
merged.get("model_library", {}),
|
|
915
|
+
)
|
|
916
|
+
configured_catalog_path = model_library_section.get("path")
|
|
917
|
+
if canonical_llm or canonical_embeddings:
|
|
918
|
+
if yaml_path is None or not isinstance(configured_catalog_path, str):
|
|
919
|
+
raise ValueError(
|
|
920
|
+
"canonical service bindings require model_library.path in a "
|
|
921
|
+
"Runtime YAML file"
|
|
922
|
+
)
|
|
923
|
+
loaded = load_model_library(
|
|
924
|
+
configured_catalog_path,
|
|
925
|
+
runtime_config_path=yaml_path,
|
|
926
|
+
environment=env_map,
|
|
927
|
+
allow_env_override=allow_env,
|
|
928
|
+
)
|
|
929
|
+
flat["model_library_path"] = str(loaded.path)
|
|
930
|
+
flat["model_library"] = loaded.library
|
|
931
|
+
flat["llm_service_specs"] = {
|
|
932
|
+
name: resolve_chat_service(loaded.library, binding, env_map)
|
|
933
|
+
for name, binding in canonical_llm.items()
|
|
934
|
+
}
|
|
935
|
+
flat["embedding_service_specs"] = {
|
|
936
|
+
name: resolve_embedding_service(loaded.library, binding, env_map)
|
|
937
|
+
for name, binding in canonical_embeddings.items()
|
|
938
|
+
}
|
|
939
|
+
elif configured_catalog_path is not None:
|
|
940
|
+
if yaml_path is None or not isinstance(configured_catalog_path, str):
|
|
941
|
+
raise ValueError("model_library.path must be a string in Runtime YAML")
|
|
942
|
+
loaded = load_model_library(
|
|
943
|
+
configured_catalog_path,
|
|
944
|
+
runtime_config_path=yaml_path,
|
|
945
|
+
environment=env_map,
|
|
946
|
+
allow_env_override=allow_env,
|
|
947
|
+
)
|
|
948
|
+
flat["model_library_path"] = str(loaded.path)
|
|
949
|
+
flat["model_library"] = loaded.library
|
|
950
|
+
|
|
951
|
+
return cls(**flat)
|
|
822
952
|
|
|
823
953
|
@classmethod
|
|
824
954
|
def _default_source_data(cls) -> dict[str, Any]:
|
|
@@ -832,6 +962,7 @@ class RuntimeConfig:
|
|
|
832
962
|
"workflow_mode": "canonical",
|
|
833
963
|
"unknown_key_policy": "fail",
|
|
834
964
|
},
|
|
965
|
+
"model_library": {"path": None},
|
|
835
966
|
"llm": {
|
|
836
967
|
"provider": "openai",
|
|
837
968
|
"model": "gpt-4",
|
|
@@ -1122,6 +1253,7 @@ class RuntimeConfig:
|
|
|
1122
1253
|
@classmethod
|
|
1123
1254
|
def _validate_yaml_keys(cls, payload: Mapping[str, Any]) -> None:
|
|
1124
1255
|
allowed_top_level = {
|
|
1256
|
+
"model_library",
|
|
1125
1257
|
"llm_pool",
|
|
1126
1258
|
"llm",
|
|
1127
1259
|
"embedding_pool",
|
|
@@ -1143,6 +1275,7 @@ class RuntimeConfig:
|
|
|
1143
1275
|
raise ValueError(f"Unknown runtime config keys: {sorted(unknown_top)}")
|
|
1144
1276
|
|
|
1145
1277
|
section_keys: dict[str, set[str]] = {
|
|
1278
|
+
"model_library": {"path"},
|
|
1146
1279
|
"llm": {"provider", "model", "api_key", "api_base"},
|
|
1147
1280
|
"embedding": {"model"},
|
|
1148
1281
|
"prompting": {"base_system_prompt"},
|
|
@@ -1199,6 +1332,12 @@ class RuntimeConfig:
|
|
|
1199
1332
|
if not isinstance(role_bindings, dict):
|
|
1200
1333
|
raise ValueError("agents.role_bindings must be an object")
|
|
1201
1334
|
|
|
1335
|
+
model_library = payload.get("model_library", {})
|
|
1336
|
+
if isinstance(model_library, dict):
|
|
1337
|
+
path_value = model_library.get("path")
|
|
1338
|
+
if path_value is not None and not isinstance(path_value, str):
|
|
1339
|
+
raise ValueError("model_library.path must be a string")
|
|
1340
|
+
|
|
1202
1341
|
tools = payload.get("tools", {})
|
|
1203
1342
|
if isinstance(tools, dict):
|
|
1204
1343
|
providers = tools.get("providers")
|
|
@@ -1269,11 +1408,28 @@ class RuntimeConfig:
|
|
|
1269
1408
|
services = llm_pool["services"]
|
|
1270
1409
|
if not isinstance(services, dict):
|
|
1271
1410
|
raise ValueError("llm_pool.services must be an object")
|
|
1272
|
-
|
|
1411
|
+
allowed_legacy_service_keys = {
|
|
1412
|
+
"provider",
|
|
1413
|
+
"model",
|
|
1414
|
+
"api_key",
|
|
1415
|
+
"api_base",
|
|
1416
|
+
"resilience",
|
|
1417
|
+
}
|
|
1418
|
+
allowed_profile_service_keys = {
|
|
1419
|
+
"profile",
|
|
1420
|
+
"credential",
|
|
1421
|
+
"resilience_overrides",
|
|
1422
|
+
"generation_overrides",
|
|
1423
|
+
}
|
|
1273
1424
|
allowed_resilience_keys = {"timeout_seconds", "max_retries"}
|
|
1274
1425
|
for service_name, config in services.items():
|
|
1275
1426
|
if not isinstance(config, dict):
|
|
1276
1427
|
raise ValueError(f"llm_pool.services.{service_name} must be an object")
|
|
1428
|
+
allowed_service_keys = (
|
|
1429
|
+
allowed_profile_service_keys
|
|
1430
|
+
if "profile" in config
|
|
1431
|
+
else allowed_legacy_service_keys
|
|
1432
|
+
)
|
|
1277
1433
|
unknown_service = set(config) - allowed_service_keys
|
|
1278
1434
|
if unknown_service:
|
|
1279
1435
|
raise ValueError(
|
|
@@ -1298,13 +1454,29 @@ class RuntimeConfig:
|
|
|
1298
1454
|
services = embedding_pool["services"]
|
|
1299
1455
|
if not isinstance(services, dict):
|
|
1300
1456
|
raise ValueError("embedding_pool.services must be an object")
|
|
1301
|
-
|
|
1457
|
+
allowed_legacy_service_keys = {
|
|
1458
|
+
"provider",
|
|
1459
|
+
"model",
|
|
1460
|
+
"api_key",
|
|
1461
|
+
"api_base",
|
|
1462
|
+
"resilience",
|
|
1463
|
+
}
|
|
1464
|
+
allowed_profile_service_keys = {
|
|
1465
|
+
"profile",
|
|
1466
|
+
"credential",
|
|
1467
|
+
"resilience_overrides",
|
|
1468
|
+
}
|
|
1302
1469
|
allowed_resilience_keys = {"timeout_seconds", "max_retries"}
|
|
1303
1470
|
for service_name, config in services.items():
|
|
1304
1471
|
if not isinstance(config, dict):
|
|
1305
1472
|
raise ValueError(
|
|
1306
1473
|
f"embedding_pool.services.{service_name} must be an object"
|
|
1307
1474
|
)
|
|
1475
|
+
allowed_service_keys = (
|
|
1476
|
+
allowed_profile_service_keys
|
|
1477
|
+
if "profile" in config
|
|
1478
|
+
else allowed_legacy_service_keys
|
|
1479
|
+
)
|
|
1308
1480
|
unknown_service = set(config) - allowed_service_keys
|
|
1309
1481
|
if unknown_service:
|
|
1310
1482
|
raise ValueError(
|
|
@@ -1545,14 +1717,18 @@ class RuntimeConfig:
|
|
|
1545
1717
|
"llm_api_base": data["llm"].get("api_base"),
|
|
1546
1718
|
"llm_pool_default_service": default_service_name,
|
|
1547
1719
|
"llm_pool_services": {
|
|
1548
|
-
name:
|
|
1720
|
+
name: cls._coerce_service_config(
|
|
1721
|
+
cast(Mapping[str, Any], config)
|
|
1722
|
+
)
|
|
1549
1723
|
for name, config in llm_services.items()
|
|
1550
1724
|
},
|
|
1551
1725
|
"embedding_pool_default_service": embedding_pool_data.get(
|
|
1552
1726
|
"default_service", "default"
|
|
1553
1727
|
),
|
|
1554
1728
|
"embedding_pool_services": {
|
|
1555
|
-
name:
|
|
1729
|
+
name: cls._coerce_embedding_service_config(
|
|
1730
|
+
cast(Mapping[str, Any], config)
|
|
1731
|
+
)
|
|
1556
1732
|
for name, config in embedding_services.items()
|
|
1557
1733
|
},
|
|
1558
1734
|
"embedding_model": data["embedding"]["model"],
|
{friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/src/friday_runtime/di.py
RENAMED
|
@@ -42,7 +42,11 @@ from friday_memory import (
|
|
|
42
42
|
)
|
|
43
43
|
from friday_transcript import SQLiteTranscriptStore
|
|
44
44
|
|
|
45
|
-
from friday_runtime.config import
|
|
45
|
+
from friday_runtime.config import (
|
|
46
|
+
EmbeddingServiceConfig,
|
|
47
|
+
LLMServiceConfig,
|
|
48
|
+
RuntimeConfig,
|
|
49
|
+
)
|
|
46
50
|
from friday_runtime.tool_loading import register_runtime_tools
|
|
47
51
|
|
|
48
52
|
logger = logging.getLogger(__name__)
|
|
@@ -136,49 +140,78 @@ class DIContainer:
|
|
|
136
140
|
"""Create LLM and embedding services."""
|
|
137
141
|
self.llm_pool = {}
|
|
138
142
|
for service_name, service_config in self._config.llm_pool.services.items():
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
chat_spec = self._config.llm_service_specs.get(service_name)
|
|
144
|
+
if chat_spec is not None:
|
|
145
|
+
self.llm_pool[service_name] = LiteLLMService.from_spec(
|
|
146
|
+
chat_spec,
|
|
147
|
+
telemetry=self.telemetry,
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
if not isinstance(service_config, LLMServiceConfig):
|
|
151
|
+
raise RuntimeError(
|
|
152
|
+
f"LLM service {service_name!r} has no resolved specification"
|
|
153
|
+
)
|
|
154
|
+
self.llm_pool[service_name] = LiteLLMService(
|
|
155
|
+
model_name=service_config.model,
|
|
156
|
+
api_key=service_config.api_key,
|
|
157
|
+
api_base=service_config.api_base,
|
|
158
|
+
telemetry=self.telemetry,
|
|
159
|
+
resilience=ResilienceConfig(
|
|
160
|
+
max_retries=service_config.resilience.max_retries,
|
|
161
|
+
timeout_seconds=float(
|
|
162
|
+
service_config.resilience.timeout_seconds
|
|
163
|
+
),
|
|
164
|
+
),
|
|
165
|
+
)
|
|
149
166
|
|
|
150
167
|
self.llm = self.llm_pool[self._config.llm_pool.default_service]
|
|
151
|
-
default_service = self._config.get_llm_service_config()
|
|
152
168
|
|
|
153
169
|
self.embedding_pool = {}
|
|
154
170
|
for (
|
|
155
171
|
service_name,
|
|
156
172
|
embedding_service_config,
|
|
157
173
|
) in self._config.embedding_pool.services.items():
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
embedding_spec = self._config.embedding_service_specs.get(service_name)
|
|
175
|
+
if embedding_spec is not None:
|
|
176
|
+
self.embedding_pool[service_name] = (
|
|
177
|
+
LiteLLMEmbeddingService.from_spec(
|
|
178
|
+
embedding_spec,
|
|
179
|
+
telemetry=self.telemetry,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
if not isinstance(
|
|
184
|
+
embedding_service_config,
|
|
185
|
+
EmbeddingServiceConfig,
|
|
186
|
+
):
|
|
187
|
+
raise RuntimeError(
|
|
188
|
+
f"Embedding service {service_name!r} has no resolved "
|
|
189
|
+
"specification"
|
|
190
|
+
)
|
|
191
|
+
self.embedding_pool[service_name] = LiteLLMEmbeddingService(
|
|
192
|
+
model_name=embedding_service_config.model,
|
|
193
|
+
api_key=embedding_service_config.api_key,
|
|
194
|
+
api_base=embedding_service_config.api_base,
|
|
195
|
+
telemetry=self.telemetry,
|
|
196
|
+
resilience=ResilienceConfig(
|
|
197
|
+
max_retries=(
|
|
198
|
+
embedding_service_config.resilience.max_retries
|
|
199
|
+
),
|
|
200
|
+
timeout_seconds=float(
|
|
201
|
+
embedding_service_config.resilience.timeout_seconds
|
|
202
|
+
),
|
|
167
203
|
),
|
|
168
|
-
)
|
|
169
|
-
)
|
|
204
|
+
)
|
|
170
205
|
|
|
171
206
|
self.embeddings = self.embedding_pool[self._config.embedding_pool.default_service]
|
|
172
|
-
default_embedding_service = self._config.get_embedding_service_config()
|
|
173
|
-
|
|
174
207
|
logger.info(
|
|
175
208
|
"LLM pool configured with default service '%s' (%s) across %d services; "
|
|
176
209
|
"embedding pool default '%s' (%s) across %d services",
|
|
177
210
|
self._config.llm_pool.default_service,
|
|
178
|
-
|
|
211
|
+
self.llm.model,
|
|
179
212
|
len(self.llm_pool),
|
|
180
213
|
self._config.embedding_pool.default_service,
|
|
181
|
-
|
|
214
|
+
self.embeddings.model,
|
|
182
215
|
len(self.embedding_pool),
|
|
183
216
|
)
|
|
184
217
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Strict local-file loading for Friday LLM model libraries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
from friday_llm import ModelLibrary
|
|
12
|
+
|
|
13
|
+
MAX_MODEL_LIBRARY_BYTES = 1024 * 1024
|
|
14
|
+
MAX_MODEL_LIBRARY_DEPTH = 12
|
|
15
|
+
MAX_MODEL_LIBRARY_NODES = 10_000
|
|
16
|
+
MAX_MODEL_LIBRARY_STRING = 4096
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ModelLibraryLoadError(ValueError):
|
|
20
|
+
"""Raised when a model-library file violates Runtime loading policy."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class LoadedModelLibrary:
|
|
25
|
+
"""Validated catalog and the canonical path used to load it."""
|
|
26
|
+
|
|
27
|
+
path: Path
|
|
28
|
+
library: ModelLibrary
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _StrictLoader(yaml.SafeLoader):
|
|
32
|
+
"""Safe YAML loader that rejects duplicate mapping keys."""
|
|
33
|
+
|
|
34
|
+
def construct_mapping(
|
|
35
|
+
self,
|
|
36
|
+
node: yaml.MappingNode,
|
|
37
|
+
deep: bool = False,
|
|
38
|
+
) -> dict[object, object]:
|
|
39
|
+
seen: set[object] = set()
|
|
40
|
+
for key_node, _ in node.value:
|
|
41
|
+
if key_node.tag == "tag:yaml.org,2002:merge":
|
|
42
|
+
raise ModelLibraryLoadError("YAML merge keys are not allowed")
|
|
43
|
+
key = self.construct_object(key_node, deep=False)
|
|
44
|
+
try:
|
|
45
|
+
duplicate = key in seen
|
|
46
|
+
except TypeError as error:
|
|
47
|
+
raise ModelLibraryLoadError("mapping keys must be scalar") from error
|
|
48
|
+
if duplicate:
|
|
49
|
+
raise ModelLibraryLoadError(f"duplicate YAML mapping key: {key!r}")
|
|
50
|
+
seen.add(key)
|
|
51
|
+
return super().construct_mapping(node, deep=deep)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_model_library(
|
|
55
|
+
configured_path: str,
|
|
56
|
+
*,
|
|
57
|
+
runtime_config_path: str | Path,
|
|
58
|
+
environment: Mapping[str, str],
|
|
59
|
+
allow_env_override: bool,
|
|
60
|
+
) -> LoadedModelLibrary:
|
|
61
|
+
"""Load one bounded local model library relative to its Runtime YAML."""
|
|
62
|
+
|
|
63
|
+
selected_path = configured_path
|
|
64
|
+
if allow_env_override:
|
|
65
|
+
override = environment.get("FRIDAY_MODELS_CONFIG")
|
|
66
|
+
if override is not None and override.strip():
|
|
67
|
+
selected_path = override
|
|
68
|
+
resolved_path = _resolve_catalog_path(
|
|
69
|
+
selected_path,
|
|
70
|
+
runtime_config_path=Path(runtime_config_path),
|
|
71
|
+
)
|
|
72
|
+
try:
|
|
73
|
+
file_size = resolved_path.stat().st_size
|
|
74
|
+
except OSError as error:
|
|
75
|
+
raise ModelLibraryLoadError(
|
|
76
|
+
f"unable to inspect model library: {resolved_path}"
|
|
77
|
+
) from error
|
|
78
|
+
if file_size > MAX_MODEL_LIBRARY_BYTES:
|
|
79
|
+
raise ModelLibraryLoadError(
|
|
80
|
+
f"model library exceeds {MAX_MODEL_LIBRARY_BYTES} bytes"
|
|
81
|
+
)
|
|
82
|
+
try:
|
|
83
|
+
document = resolved_path.read_text(encoding="utf-8")
|
|
84
|
+
except (OSError, UnicodeError) as error:
|
|
85
|
+
raise ModelLibraryLoadError(
|
|
86
|
+
f"unable to read model library: {resolved_path}"
|
|
87
|
+
) from error
|
|
88
|
+
_reject_yaml_references(document)
|
|
89
|
+
try:
|
|
90
|
+
payload = yaml.load(document, Loader=_StrictLoader)
|
|
91
|
+
except ModelLibraryLoadError:
|
|
92
|
+
raise
|
|
93
|
+
except yaml.YAMLError as error:
|
|
94
|
+
raise ModelLibraryLoadError("invalid model-library YAML") from error
|
|
95
|
+
if not isinstance(payload, dict):
|
|
96
|
+
raise ModelLibraryLoadError("model library must be a YAML mapping")
|
|
97
|
+
_validate_structure_bounds(payload)
|
|
98
|
+
try:
|
|
99
|
+
library = ModelLibrary.model_validate(payload)
|
|
100
|
+
except ValueError as error:
|
|
101
|
+
raise ModelLibraryLoadError("invalid model-library schema") from error
|
|
102
|
+
return LoadedModelLibrary(path=resolved_path, library=library)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _resolve_catalog_path(
|
|
106
|
+
configured_path: str,
|
|
107
|
+
*,
|
|
108
|
+
runtime_config_path: Path,
|
|
109
|
+
) -> Path:
|
|
110
|
+
if not configured_path.strip():
|
|
111
|
+
raise ModelLibraryLoadError("model_library.path must be non-empty")
|
|
112
|
+
parsed = urlsplit(configured_path)
|
|
113
|
+
if parsed.scheme or parsed.netloc:
|
|
114
|
+
raise ModelLibraryLoadError("model_library.path must be a local file path")
|
|
115
|
+
raw_path = Path(configured_path).expanduser()
|
|
116
|
+
candidate = (
|
|
117
|
+
raw_path
|
|
118
|
+
if raw_path.is_absolute()
|
|
119
|
+
else runtime_config_path.expanduser().resolve().parent / raw_path
|
|
120
|
+
)
|
|
121
|
+
try:
|
|
122
|
+
resolved = candidate.resolve(strict=True)
|
|
123
|
+
except OSError as error:
|
|
124
|
+
raise ModelLibraryLoadError(
|
|
125
|
+
f"model library does not exist: {candidate}"
|
|
126
|
+
) from error
|
|
127
|
+
if not resolved.is_file():
|
|
128
|
+
raise ModelLibraryLoadError(
|
|
129
|
+
f"model library is not a regular file: {resolved}"
|
|
130
|
+
)
|
|
131
|
+
return resolved
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _reject_yaml_references(document: str) -> None:
|
|
135
|
+
try:
|
|
136
|
+
events = yaml.parse(document, Loader=yaml.SafeLoader)
|
|
137
|
+
for event in events:
|
|
138
|
+
if isinstance(event, yaml.AliasEvent) or getattr(event, "anchor", None):
|
|
139
|
+
raise ModelLibraryLoadError(
|
|
140
|
+
"YAML anchors and aliases are not allowed"
|
|
141
|
+
)
|
|
142
|
+
except ModelLibraryLoadError:
|
|
143
|
+
raise
|
|
144
|
+
except yaml.YAMLError as error:
|
|
145
|
+
raise ModelLibraryLoadError("invalid model-library YAML") from error
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _validate_structure_bounds(payload: dict[object, object]) -> None:
|
|
149
|
+
nodes = 0
|
|
150
|
+
pending: list[tuple[object, int]] = [(payload, 1)]
|
|
151
|
+
while pending:
|
|
152
|
+
value, depth = pending.pop()
|
|
153
|
+
nodes += 1
|
|
154
|
+
if nodes > MAX_MODEL_LIBRARY_NODES:
|
|
155
|
+
raise ModelLibraryLoadError(
|
|
156
|
+
f"model library exceeds {MAX_MODEL_LIBRARY_NODES} YAML nodes"
|
|
157
|
+
)
|
|
158
|
+
if depth > MAX_MODEL_LIBRARY_DEPTH:
|
|
159
|
+
raise ModelLibraryLoadError(
|
|
160
|
+
f"model library exceeds YAML depth {MAX_MODEL_LIBRARY_DEPTH}"
|
|
161
|
+
)
|
|
162
|
+
if isinstance(value, str) and len(value) > MAX_MODEL_LIBRARY_STRING:
|
|
163
|
+
raise ModelLibraryLoadError(
|
|
164
|
+
f"model library string exceeds {MAX_MODEL_LIBRARY_STRING} characters"
|
|
165
|
+
)
|
|
166
|
+
if isinstance(value, dict):
|
|
167
|
+
for key, child in value.items():
|
|
168
|
+
pending.append((key, depth + 1))
|
|
169
|
+
pending.append((child, depth + 1))
|
|
170
|
+
elif isinstance(value, list):
|
|
171
|
+
for child in value:
|
|
172
|
+
pending.append((child, depth + 1))
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Runtime model-library file-policy tests."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from friday_core.profiles import MemoryProfile
|
|
7
|
+
from friday_runtime.config import RuntimeConfig
|
|
8
|
+
from friday_runtime.di import DIContainer
|
|
9
|
+
from friday_runtime.model_library import ModelLibraryLoadError, load_model_library
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _write_catalog(path: Path, *, marker: str = "local") -> None:
|
|
13
|
+
path.write_text(
|
|
14
|
+
f"""
|
|
15
|
+
schema: friday.model-library/v1
|
|
16
|
+
endpoints:
|
|
17
|
+
{marker}:
|
|
18
|
+
api_base: http://127.0.0.1:1234/v1
|
|
19
|
+
provider: openai_compatible
|
|
20
|
+
profiles:
|
|
21
|
+
chat:
|
|
22
|
+
kind: chat
|
|
23
|
+
endpoint: {marker}
|
|
24
|
+
dialect: openai_chat_completions
|
|
25
|
+
model: openai/qwen3.8
|
|
26
|
+
model_target:
|
|
27
|
+
model_id: qwen3.8
|
|
28
|
+
family: qwen3_8
|
|
29
|
+
revision: exact
|
|
30
|
+
""".strip()
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_relative_catalog_path_is_resolved_against_runtime_yaml(tmp_path: Path) -> None:
|
|
35
|
+
config_dir = tmp_path / "configuration"
|
|
36
|
+
config_dir.mkdir()
|
|
37
|
+
runtime_path = config_dir / "config.yaml"
|
|
38
|
+
runtime_path.write_text("model_library:\n path: models-config.yaml\n")
|
|
39
|
+
catalog_path = config_dir / "models-config.yaml"
|
|
40
|
+
_write_catalog(catalog_path)
|
|
41
|
+
|
|
42
|
+
loaded = load_model_library(
|
|
43
|
+
"models-config.yaml",
|
|
44
|
+
runtime_config_path=runtime_path,
|
|
45
|
+
environment={},
|
|
46
|
+
allow_env_override=False,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
assert loaded.path == catalog_path.resolve()
|
|
50
|
+
assert "chat" in loaded.library.profiles
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_environment_path_override_requires_policy_opt_in(tmp_path: Path) -> None:
|
|
54
|
+
runtime_path = tmp_path / "config.yaml"
|
|
55
|
+
default_path = tmp_path / "default.yaml"
|
|
56
|
+
override_path = tmp_path / "override.yaml"
|
|
57
|
+
_write_catalog(default_path, marker="default")
|
|
58
|
+
_write_catalog(override_path, marker="override")
|
|
59
|
+
environment = {"FRIDAY_MODELS_CONFIG": str(override_path)}
|
|
60
|
+
|
|
61
|
+
disabled = load_model_library(
|
|
62
|
+
str(default_path),
|
|
63
|
+
runtime_config_path=runtime_path,
|
|
64
|
+
environment=environment,
|
|
65
|
+
allow_env_override=False,
|
|
66
|
+
)
|
|
67
|
+
enabled = load_model_library(
|
|
68
|
+
str(default_path),
|
|
69
|
+
runtime_config_path=runtime_path,
|
|
70
|
+
environment=environment,
|
|
71
|
+
allow_env_override=True,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
assert disabled.path == default_path.resolve()
|
|
75
|
+
assert enabled.path == override_path.resolve()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.parametrize(
|
|
79
|
+
"document",
|
|
80
|
+
(
|
|
81
|
+
"schema: friday.model-library/v1\nschema: friday.model-library/v1\n",
|
|
82
|
+
"schema: &schema friday.model-library/v1\ncopy: *schema\n",
|
|
83
|
+
"schema: friday.model-library/v1\nvalue: !custom tagged\n",
|
|
84
|
+
"schema: friday.model-library/v1\nbase: &base {provider: openai}\nmerged: {<<: *base}\n",
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
def test_strict_loader_rejects_ambiguous_yaml(
|
|
88
|
+
tmp_path: Path,
|
|
89
|
+
document: str,
|
|
90
|
+
) -> None:
|
|
91
|
+
catalog_path = tmp_path / "models.yaml"
|
|
92
|
+
catalog_path.write_text(document)
|
|
93
|
+
|
|
94
|
+
with pytest.raises(ModelLibraryLoadError):
|
|
95
|
+
load_model_library(
|
|
96
|
+
str(catalog_path),
|
|
97
|
+
runtime_config_path=tmp_path / "config.yaml",
|
|
98
|
+
environment={},
|
|
99
|
+
allow_env_override=False,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_remote_catalog_is_rejected(tmp_path: Path) -> None:
|
|
104
|
+
with pytest.raises(ModelLibraryLoadError, match="local file"):
|
|
105
|
+
load_model_library(
|
|
106
|
+
"https://example.test/models.yaml",
|
|
107
|
+
runtime_config_path=tmp_path / "config.yaml",
|
|
108
|
+
environment={},
|
|
109
|
+
allow_env_override=False,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_runtime_resolves_canonical_pools_and_di_uses_factories(
|
|
114
|
+
tmp_path: Path,
|
|
115
|
+
) -> None:
|
|
116
|
+
install_root = tmp_path / "install"
|
|
117
|
+
runtime_parent = tmp_path / "runtime"
|
|
118
|
+
workspace_root = tmp_path / "workspace"
|
|
119
|
+
install_root.mkdir()
|
|
120
|
+
runtime_parent.mkdir()
|
|
121
|
+
workspace_root.mkdir()
|
|
122
|
+
catalog_path = tmp_path / "models-config.yaml"
|
|
123
|
+
catalog_path.write_text(
|
|
124
|
+
"""
|
|
125
|
+
schema: friday.model-library/v1
|
|
126
|
+
credentials:
|
|
127
|
+
local_key:
|
|
128
|
+
env: FRIDAY_TEST_LLM_KEY
|
|
129
|
+
endpoints:
|
|
130
|
+
local:
|
|
131
|
+
api_base: http://127.0.0.1:1234/v1
|
|
132
|
+
provider: openai_compatible
|
|
133
|
+
serving_engine: lm_studio
|
|
134
|
+
default_credential: local_key
|
|
135
|
+
profiles:
|
|
136
|
+
chat:
|
|
137
|
+
kind: chat
|
|
138
|
+
endpoint: local
|
|
139
|
+
dialect: openai_chat_completions
|
|
140
|
+
model: openai/qwen3.8
|
|
141
|
+
model_target:
|
|
142
|
+
model_id: qwen3.8
|
|
143
|
+
family: qwen3_8
|
|
144
|
+
revision: exact
|
|
145
|
+
embedding:
|
|
146
|
+
kind: embedding
|
|
147
|
+
endpoint: local
|
|
148
|
+
dialect: openai_embeddings
|
|
149
|
+
model: openai/nomic-embed
|
|
150
|
+
model_target:
|
|
151
|
+
model_id: nomic-embed
|
|
152
|
+
family: nomic_embed
|
|
153
|
+
revision: exact
|
|
154
|
+
""".strip()
|
|
155
|
+
)
|
|
156
|
+
runtime_path = tmp_path / "config.yaml"
|
|
157
|
+
runtime_path.write_text(
|
|
158
|
+
f"""
|
|
159
|
+
model_library:
|
|
160
|
+
path: ./models-config.yaml
|
|
161
|
+
runtime_paths:
|
|
162
|
+
install_root: {install_root}
|
|
163
|
+
runtime_data_root: {runtime_parent / "friday"}
|
|
164
|
+
workspace_root: {workspace_root}
|
|
165
|
+
llm_pool:
|
|
166
|
+
default_service: default
|
|
167
|
+
services:
|
|
168
|
+
default:
|
|
169
|
+
profile: chat
|
|
170
|
+
embedding_pool:
|
|
171
|
+
default_service: default
|
|
172
|
+
services:
|
|
173
|
+
default:
|
|
174
|
+
profile: embedding
|
|
175
|
+
""".strip()
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
config = RuntimeConfig.from_sources(
|
|
179
|
+
yaml_path=runtime_path,
|
|
180
|
+
env={"FRIDAY_TEST_LLM_KEY": "redacted-local-key"},
|
|
181
|
+
)
|
|
182
|
+
container = DIContainer(config, MemoryProfile.FULL)
|
|
183
|
+
container._create_llm_services()
|
|
184
|
+
|
|
185
|
+
assert config.model_library_path == str(catalog_path.resolve())
|
|
186
|
+
assert config.llm_api_key is None
|
|
187
|
+
assert container.llm is not None
|
|
188
|
+
assert container.llm.model == "openai/qwen3.8"
|
|
189
|
+
assert container.llm.api_key == "redacted-local-key"
|
|
190
|
+
assert container.llm.execution_target is not None
|
|
191
|
+
assert container.embeddings is not None
|
|
192
|
+
assert container.embeddings.execution_target is not None
|
|
File without changes
|
|
File without changes
|
{friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/src/friday_runtime/__init__.py
RENAMED
|
File without changes
|
{friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/src/friday_runtime/kernel.py
RENAMED
|
File without changes
|
|
File without changes
|
{friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/tests_runtime/__init__.py
RENAMED
|
File without changes
|
{friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/tests_runtime/test_config.py
RENAMED
|
File without changes
|
{friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/tests_runtime/test_di.py
RENAMED
|
File without changes
|
{friday_framework_runtime-0.1.0a0 → friday_framework_runtime-0.1.0a2}/tests_runtime/test_kernel.py
RENAMED
|
File without changes
|