classJsonArtifactStore(ArtifactStoreProtocol):""" File-based artifact store that persists artifacts as JSON files. Directory structure: artifacts_dir/ {run_id}/ {artifact_id}.json """def__init__(self,root_dir:str|Path)->None:self.root_dir=Path(root_dir)self.root_dir.mkdir(parents=True,exist_ok=True)# artifact_id -> path, filled as artifacts are written or looked up, so a# repeat lookup does not re-scan every run directory (O-16).self._path_index:dict[str,Path]={}asyncdefwrite_artifact(self,artifact:ArtifactEnvelope[ArtifactPayload])->None:"""Write artifact to JSON file."""run_dir=self.root_dir/artifact.run_idrun_dir.mkdir(parents=True,exist_ok=True)path=run_dir/f"{artifact.artifact_id}.json"path.write_text(json.dumps(artifact.model_dump(mode="json",by_alias=True),indent=2),encoding="utf-8",)self._path_index[artifact.artifact_id]=pathasyncdefget_artifact(self,artifact_id:str)->ArtifactEnvelope[ArtifactPayload]|None:"""Read an artifact by id, searching every run directory if need be. Artifacts are stored under ``{run_id}/{artifact_id}.json`` and the run id is not recoverable from the artifact id, so the first lookup for an id written by another process has to scan. The resolved path is cached. """cached=self._path_index.get(artifact_id)ifcachedisnotNoneandcached.exists():returnArtifactEnvelope.model_validate_json(cached.read_text(encoding="utf-8"))forrun_dirinself.root_dir.iterdir():ifnotrun_dir.is_dir():continuepath=run_dir/f"{artifact_id}.json"ifpath.exists():self._path_index[artifact_id]=pathreturnArtifactEnvelope.model_validate_json(path.read_text(encoding="utf-8"))returnNoneasyncdeflist_run_artifacts(self,run_id:str)->list[ArtifactEnvelope[ArtifactPayload]]:"""List all artifacts for a given run."""run_dir=self.root_dir/run_idifnotrun_dir.exists():return[]artifacts=[]forpathinrun_dir.glob("*.json"):try:artifact=ArtifactEnvelope.model_validate_json(path.read_text(encoding="utf-8"))artifacts.append(artifact)exceptException:# Skip corrupted filescontinuereturnartifactsasyncdeflist_phase_artifacts(self,run_id:str,phase_name:str)->list[ArtifactEnvelope[ArtifactPayload]]:"""List all artifacts for a given run and phase."""all_artifacts=awaitself.list_run_artifacts(run_id)return[aforainall_artifactsifa.phase_name==phase_name]
Read an artifact by id, searching every run directory if need be.
Artifacts are stored under {run_id}/{artifact_id}.json and the run id
is not recoverable from the artifact id, so the first lookup for an id
written by another process has to scan. The resolved path is cached.
Source code in packages/episteme-pipeline/episteme_pipeline/artifacts/store.py
asyncdefget_artifact(self,artifact_id:str)->ArtifactEnvelope[ArtifactPayload]|None:"""Read an artifact by id, searching every run directory if need be. Artifacts are stored under ``{run_id}/{artifact_id}.json`` and the run id is not recoverable from the artifact id, so the first lookup for an id written by another process has to scan. The resolved path is cached. """cached=self._path_index.get(artifact_id)ifcachedisnotNoneandcached.exists():returnArtifactEnvelope.model_validate_json(cached.read_text(encoding="utf-8"))forrun_dirinself.root_dir.iterdir():ifnotrun_dir.is_dir():continuepath=run_dir/f"{artifact_id}.json"ifpath.exists():self._path_index[artifact_id]=pathreturnArtifactEnvelope.model_validate_json(path.read_text(encoding="utf-8"))returnNone
Source code in packages/episteme-pipeline/episteme_pipeline/artifacts/store.py
878889909192
asyncdeflist_phase_artifacts(self,run_id:str,phase_name:str)->list[ArtifactEnvelope[ArtifactPayload]]:"""List all artifacts for a given run and phase."""all_artifacts=awaitself.list_run_artifacts(run_id)return[aforainall_artifactsifa.phase_name==phase_name]
Source code in packages/episteme-pipeline/episteme_pipeline/artifacts/store.py
67686970717273747576777879808182838485
asyncdeflist_run_artifacts(self,run_id:str)->list[ArtifactEnvelope[ArtifactPayload]]:"""List all artifacts for a given run."""run_dir=self.root_dir/run_idifnotrun_dir.exists():return[]artifacts=[]forpathinrun_dir.glob("*.json"):try:artifact=ArtifactEnvelope.model_validate_json(path.read_text(encoding="utf-8"))artifacts.append(artifact)exceptException:# Skip corrupted filescontinuereturnartifacts
Source code in packages/episteme-pipeline/episteme_pipeline/artifacts/store.py
2930313233343536373839
asyncdefwrite_artifact(self,artifact:ArtifactEnvelope[ArtifactPayload])->None:"""Write artifact to JSON file."""run_dir=self.root_dir/artifact.run_idrun_dir.mkdir(parents=True,exist_ok=True)path=run_dir/f"{artifact.artifact_id}.json"path.write_text(json.dumps(artifact.model_dump(mode="json",by_alias=True),indent=2),encoding="utf-8",)self._path_index[artifact.artifact_id]=path
Compute a fingerprint for a method object (e.g., LLM, embedding model, extractor).
Prefers an explicit contract: if the object exposes fingerprint()
returning a JSON-serialisable value, that value is the fingerprint input.
Implement it on any component whose behaviour depends on state this module
cannot see — a SentenceTransformer revision, a quantisation setting, a
reranker's activation function.
Otherwise this falls back to probing a fixed list of well-known attributes
(model name, temperature, max_tokens, dimensions, …). That fallback is
best-effort and silently incomplete by construction (O-15): anything not on
the list is invisible to invalidation, so a run can be reused after a change
that should have invalidated it.
Returns None if the object is None or exposes nothing recognisable.
Source code in packages/episteme-pipeline/episteme_pipeline/runs/fingerprints.py
deffingerprint_method(method_obj:Any)->str|None:"""Compute a fingerprint for a method object (e.g., LLM, embedding model, extractor). Prefers an explicit contract: if the object exposes ``fingerprint()`` returning a JSON-serialisable value, that value *is* the fingerprint input. Implement it on any component whose behaviour depends on state this module cannot see — a SentenceTransformer revision, a quantisation setting, a reranker's activation function. Otherwise this falls back to probing a fixed list of well-known attributes (model name, temperature, max_tokens, dimensions, …). That fallback is best-effort and silently incomplete by construction (O-15): anything not on the list is invisible to invalidation, so a run can be reused after a change that should have invalidated it. Returns None if the object is None or exposes nothing recognisable. """ifmethod_objisNone:returnNoneexplicit=getattr(method_obj,"fingerprint",None)ifcallable(explicit):try:value=explicit()if_is_fully_serializable(value):returnstable_fingerprint({"class":method_obj.__class__.__name__,"fingerprint":value})exceptExceptionasex:logger.exception(ex)passfingerprint_data:dict[str,Any]={"class":method_obj.__class__.__name__}model_name=getattr(method_obj,"model_name",None)ifmodel_name:fingerprint_data["model"]=str(model_name)model_name_or_provider=getattr(method_obj,"model_name_or_provider",None)ifmodel_name_or_provider:fingerprint_data["model_provider"]=str(model_name_or_provider)model=getattr(method_obj,"model",None)ifmodelisnotNone:fingerprint_data["model_ref"]=str(model)# API version detection — important for LLM API changesapi_version=getattr(method_obj,"api_version",None)ifapi_versionisnotNoneand_is_fully_serializable(api_version):fingerprint_data["api_version"]=str(api_version)# Provider/endpoint configurationbase_url=getattr(method_obj,"base_url",None)ifbase_urlisnotNoneand_is_fully_serializable(base_url):fingerprint_data["base_url"]=str(base_url)# Runtime parameters that affect model outputtemperature=getattr(method_obj,"temperature",None)iftemperatureisnotNoneand_is_fully_serializable(temperature):fingerprint_data["temperature"]=temperaturemax_tokens=getattr(method_obj,"max_tokens",None)ifmax_tokensisnotNoneand_is_fully_serializable(max_tokens):fingerprint_data["max_tokens"]=max_tokenstop_p=getattr(method_obj,"top_p",None)iftop_pisnotNoneand_is_fully_serializable(top_p):fingerprint_data["top_p"]=top_pn=getattr(method_obj,"n",None)ifnisnotNoneand_is_fully_serializable(n):fingerprint_data["n"]=n# Embedding-specific parametersdimensions=getattr(method_obj,"dimensions",None)ifdimensionsisnotNoneand_is_fully_serializable(dimensions):fingerprint_data["dimensions"]=dimensionsembedding_api_version=getattr(method_obj,"embedding_api_version",None)ifembedding_api_versionisnotNoneand_is_fully_serializable(embedding_api_version):fingerprint_data["embedding_api_version"]=str(embedding_api_version)# Catch model_kwargs / kwargs that may contain additional paramsmodel_kwargs=getattr(method_obj,"model_kwargs",None)if(model_kwargsandisinstance(model_kwargs,dict)and_is_fully_serializable(model_kwargs)):fingerprint_data["model_kwargs"]=model_kwargsreturnstable_fingerprint(fingerprint_data)
Source code in packages/episteme-pipeline/episteme_pipeline/runs/fingerprints.py