Skip to content

Commit c43ad68

Browse files
Refactor/improve architecture (Pipelex#144)
* Better singleton * Fixed major bug in identifying structure of implicit concept --------- Co-authored-by: Louis Choquel <louis@pipelex.com>
1 parent cc35fcc commit c43ad68

7 files changed

Lines changed: 59 additions & 61 deletions

File tree

pipelex/core/concept.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def extract_domain_and_concept_from_str(cls, concept_str: str) -> Tuple[str, str
106106
if "." in concept_str:
107107
domain_code, concept_code = concept_str.split(".")
108108
return domain_code, concept_code
109-
raise ConceptError(f"No extraction of domain and concept from concept code '{concept_str}'")
109+
raise ConceptError(f"Could not extract domain and concept from concept code '{concept_str}'")
110110

111111
@classmethod
112112
def extract_concept_name_from_str(cls, concept_str: str) -> str:

pipelex/core/concept_factory.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,26 +82,33 @@ def make_from_details_dict(
8282
return the_concept
8383

8484
@classmethod
85-
def make_concept_from_definition(
85+
def make_concept_from_definition_str(
8686
cls,
8787
domain_code: str,
88-
code: str,
88+
concept_str: str,
8989
definition: str,
9090
) -> Concept:
9191
structure_class_name: str
92-
if Concept.is_valid_structure_class(structure_class_name=code):
92+
refines: List[str]
93+
if Concept.concept_str_contains_domain(concept_str=concept_str):
94+
concept_name = Concept.extract_concept_name_from_str(concept_str=concept_str)
95+
else:
96+
concept_name = concept_str
97+
if Concept.is_valid_structure_class(structure_class_name=concept_name):
9398
# structure is set implicitly, by the concept's code
94-
structure_class_name = code
99+
structure_class_name = concept_name
100+
refines = []
95101
else:
96102
structure_class_name = TextContent.__name__
103+
refines = [NativeConcept.TEXT.code]
97104

98105
try:
99106
the_concept = Concept(
100-
code=ConceptCodeFactory.make_concept_code(domain_code, code),
107+
code=ConceptCodeFactory.make_concept_code(domain_code, concept_name),
101108
domain=domain_code,
102109
definition=definition,
103110
structure_class_name=structure_class_name,
104-
refines=[NativeConcept.TEXT.code],
111+
refines=refines,
105112
)
106113
return Concept.model_validate(the_concept)
107114
except ValidationError as e:

pipelex/core/concept_library.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,22 @@ def is_compatible(self, tested_concept: Concept, wanted_concept: Concept) -> boo
106106
@override
107107
def is_compatible_by_concept_code(self, tested_concept_code: str, wanted_concept_code: str) -> bool:
108108
if wanted_concept_code == NativeConcept.ANYTHING.code:
109+
log.debug(
110+
f"Concept '{tested_concept_code}' is compatible with '{wanted_concept_code}' "
111+
f"because '{wanted_concept_code}' is '{NativeConcept.ANYTHING.code}'"
112+
)
109113
return True
110114
tested_concept = self.get_required_concept(concept_code=tested_concept_code)
111115
wanted_concept = self.get_required_concept(concept_code=wanted_concept_code)
112116
if tested_concept.code == wanted_concept.code:
117+
log.debug(f"Concept '{tested_concept_code}' is compatible with '{wanted_concept_code}' because they have the same code")
113118
return True
114119
for inherited_concept_code in tested_concept.refines:
115120
if self.is_compatible_by_concept_code(inherited_concept_code, wanted_concept_code):
121+
log.debug(
122+
f"Concept '{tested_concept_code}' is compatible with '{wanted_concept_code}' "
123+
f"because '{tested_concept_code}' refines '{inherited_concept_code}' which is compatible with '{wanted_concept_code}'"
124+
)
116125
return True
117126
return False
118127

@@ -134,9 +143,9 @@ def get_required_concept(self, concept_code: str) -> Concept:
134143
if self.is_concept_implicit(concept_code=concept_code):
135144
# The implicit concept is obviously coming with a domain (the one it is used in)
136145
# TODO: replace this with a concept factory method make_implicit_concept
137-
return ConceptFactory.make_concept_from_definition(
146+
return ConceptFactory.make_concept_from_definition_str(
138147
domain_code="implicit",
139-
code=Concept.extract_domain_and_concept_from_str(concept_str=concept_code)[1],
148+
concept_str=Concept.extract_domain_and_concept_from_str(concept_str=concept_code)[1],
140149
definition=concept_code,
141150
)
142151
else:

pipelex/libraries/library_manager.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,24 +256,29 @@ def _load_library_components_from_recursive_dict(
256256
continue
257257

258258
def _load_concepts(self, domain_code: str, obj_dict: Dict[str, Any]):
259-
for concept_code, concept_obj in obj_dict.items():
259+
for concept_str, concept_obj in obj_dict.items():
260260
if isinstance(concept_obj, str):
261261
# we only have a definition
262-
concept_from_def = ConceptFactory.make_concept_from_definition(domain_code=domain_code, code=concept_code, definition=concept_obj)
262+
definition = concept_obj
263+
concept_from_def = ConceptFactory.make_concept_from_definition_str(
264+
domain_code=domain_code,
265+
concept_str=concept_str,
266+
definition=definition,
267+
)
263268
self.concept_library.add_new_concept(concept=concept_from_def)
264269
elif isinstance(concept_obj, dict):
265270
# blueprint dict definition
266271
concept_obj_dict: Dict[str, Any] = concept_obj
267272
try:
268273
concept_from_dict = ConceptFactory.make_from_details_dict(
269-
domain_code=domain_code, code=concept_code, details_dict=concept_obj_dict
274+
domain_code=domain_code, code=concept_str, details_dict=concept_obj_dict
270275
)
271276
except ValidationError as exc:
272277
error_msg = format_pydantic_validation_error(exc)
273-
raise ConceptLibraryError(f"Error loading concept '{concept_code}' because of: {error_msg}") from exc
278+
raise ConceptLibraryError(f"Error loading concept '{concept_str}' because of: {error_msg}") from exc
274279
self.concept_library.add_new_concept(concept=concept_from_dict)
275280
else:
276-
raise ConceptLibraryError(f"Unexpected type for concept_code '{concept_code}' in domain '{domain_code}': {type(concept_obj)}")
281+
raise ConceptLibraryError(f"Unexpected type for concept_code '{concept_str}' in domain '{domain_code}': {type(concept_obj)}")
277282

278283
def _load_pipes(self, domain_code: str, obj_dict: Dict[str, Any]):
279284
for pipe_code, pipe_obj in obj_dict.items():

pipelex/pipelex.py

Lines changed: 16 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from importlib.metadata import metadata
2-
from typing import Any, ClassVar, List, Optional, Type
2+
from typing import Any, List, Optional, Type, cast
33

44
from dotenv import load_dotenv
55
from kajson.class_registry import ClassRegistry
66
from kajson.class_registry_abstract import ClassRegistryAbstract
77
from kajson.kajson_manager import KajsonManager
8+
from kajson.singleton import MetaSingleton
89
from pydantic import ValidationError
910
from rich import print
1011
from typing_extensions import Self
@@ -55,39 +56,9 @@
5556
PACKAGE_VERSION = metadata(PACKAGE_NAME)["Version"]
5657

5758

58-
class Pipelex:
59-
_pipelex_instance: ClassVar[Optional[Self]] = None
60-
61-
def __new__(
62-
cls,
63-
pipelex_cls: Optional[Type[Self]] = None,
64-
pipelex_hub: Optional[PipelexHub] = None,
65-
config_cls: Optional[Type[ConfigRoot]] = None,
66-
ready_made_config: Optional[ConfigRoot] = None,
67-
class_registry: Optional[ClassRegistryAbstract] = None,
68-
template_provider: Optional[TemplateLibrary] = None,
69-
llm_model_provider: Optional[LLMModelLibrary] = None,
70-
inference_manager: Optional[InferenceManager] = None,
71-
pipeline_manager: Optional[PipelineManager] = None,
72-
pipeline_tracker: Optional[PipelineTracker] = None,
73-
activity_manager: Optional[ActivityManagerProtocol] = None,
74-
reporting_delegate: Optional[ReportingProtocol] = None,
75-
) -> Self:
76-
if cls._pipelex_instance is not None:
77-
raise RuntimeError(
78-
"Pipelex is a singleton, it is instantiated only once. Its instance is private. All you need is accesible through the hub."
79-
)
80-
if pipelex_cls is None:
81-
pipelex_cls = cls
82-
83-
if not issubclass(pipelex_cls, cls):
84-
raise TypeError(f"{pipelex_cls!r} is not a subclass of {cls.__name__}")
85-
86-
return super().__new__(pipelex_cls)
87-
59+
class Pipelex(metaclass=MetaSingleton):
8860
def __init__(
8961
self,
90-
pipelex_cls: Optional[Type[Self]] = None,
9162
pipelex_hub: Optional[PipelexHub] = None,
9263
config_cls: Optional[Type[ConfigRoot]] = None,
9364
ready_made_config: Optional[ConfigRoot] = None,
@@ -153,7 +124,11 @@ def __init__(
153124
self.pipelex_hub.set_domain_provider(domain_provider=domain_library)
154125
self.pipelex_hub.set_concept_provider(concept_provider=concept_library)
155126
self.pipelex_hub.set_pipe_provider(pipe_provider=pipe_library)
156-
self.library_manager = LibraryManager(domain_library=domain_library, concept_library=concept_library, pipe_library=pipe_library)
127+
self.library_manager = LibraryManager(
128+
domain_library=domain_library,
129+
concept_library=concept_library,
130+
pipe_library=pipe_library,
131+
)
157132
self.library_manager.setup()
158133
self.pipelex_hub.set_library_manager(library_manager=self.library_manager)
159134

@@ -178,7 +153,6 @@ def __init__(
178153
self.activity_manager = ActivityManagerNoOp()
179154
self.pipelex_hub.set_activity_manager(activity_manager=self.activity_manager)
180155

181-
Pipelex._pipelex_instance = self
182156
log.debug(f"{PACKAGE_NAME} version {PACKAGE_VERSION} init done")
183157

184158
def setup(
@@ -257,10 +231,12 @@ def teardown(self):
257231
self.class_registry.teardown()
258232
func_registry.teardown()
259233

260-
Pipelex._pipelex_instance = None
261234
project_name = get_config().project_name
262235
log.debug(f"{PACKAGE_NAME} version {PACKAGE_VERSION} teardown done for {get_config().project_name} (except config & logs)")
263236
self.pipelex_hub.reset_config()
237+
# Clear the singleton instance from metaclass
238+
if self.__class__ in MetaSingleton.instances:
239+
del MetaSingleton.instances[self.__class__]
264240
print(f"{PACKAGE_NAME} version {PACKAGE_VERSION} config reset done for {project_name}")
265241

266242
# TODO: add kwargs to make() so that subclasses can employ specific parameters
@@ -274,10 +250,12 @@ def make(cls, structure_classes: Optional[List[Type[Any]]] = None) -> Self:
274250

275251
@classmethod
276252
def get_optional_instance(cls) -> Optional[Self]:
277-
return cls._pipelex_instance
253+
instance = MetaSingleton.instances.get(cls)
254+
return cast(Optional[Self], instance)
278255

279256
@classmethod
280257
def get_instance(cls) -> Self:
281-
if cls._pipelex_instance is None:
258+
instance = MetaSingleton.instances.get(cls)
259+
if instance is None:
282260
raise RuntimeError("Pipelex is not initialized")
283-
return cls._pipelex_instance
261+
return cast(Self, instance)

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ dependencies = [
2424
"instructor>=1.8.3",
2525
"jinja2>=3.1.4",
2626
"json2html>=1.3.0",
27-
"kajson==0.2.3",
27+
"kajson @ git+https://github.com/Pipelex/kajson.git@refactor/Proper-Singleton",
2828
"markdown>=3.6",
2929
"networkx>=3.4.2",
3030
"openai>=1.60.1",
@@ -284,3 +284,6 @@ packages = ["pipelex"]
284284

285285
[tool.hatch.build.targets.sdist.force-include]
286286
"pyproject.toml" = "pipelex/pyproject.toml"
287+
288+
[tool.hatch.metadata]
289+
allow-direct-references = true

uv.lock

Lines changed: 4 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)