schematize 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.
Files changed (76) hide show
  1. schematize/__init__.py +32 -0
  2. schematize/_scripts/__init__.py +0 -0
  3. schematize/_scripts/evaluate_annotator_agreement.py +100 -0
  4. schematize/_scripts/evaluate_schema.py +169 -0
  5. schematize/_scripts/schema_generator.py +116 -0
  6. schematize/_scripts/schema_generator_ablation.py +151 -0
  7. schematize/_scripts/schema_generator_mocked.py +153 -0
  8. schematize/agents/__init__.py +0 -0
  9. schematize/agents/agent_state.py +54 -0
  10. schematize/agents/basic_agents.py +115 -0
  11. schematize/agents/chat_agent.py +85 -0
  12. schematize/agents/data_agents.py +165 -0
  13. schematize/agents/output_models.py +50 -0
  14. schematize/agents/schema_generator.py +339 -0
  15. schematize/configs/__init__.py +0 -0
  16. schematize/configs/prompt/en/law/init_chat.yaml +137 -0
  17. schematize/configs/prompt/en/law/problem_definer.yaml +52 -0
  18. schematize/configs/prompt/en/law/problem_definer_helper.yaml +119 -0
  19. schematize/configs/prompt/en/law/query_generator.yaml +32 -0
  20. schematize/configs/prompt/en/law/schema_assessment.yaml +77 -0
  21. schematize/configs/prompt/en/law/schema_data_assessment.yaml +71 -0
  22. schematize/configs/prompt/en/law/schema_data_assessment_merger.yaml +93 -0
  23. schematize/configs/prompt/en/law/schema_data_refiner.yaml +125 -0
  24. schematize/configs/prompt/en/law/schema_generator.yaml +65 -0
  25. schematize/configs/prompt/en/law/schema_refiner.yaml +88 -0
  26. schematize/configs/prompt/en/tax/init_chat.yaml +128 -0
  27. schematize/configs/prompt/en/tax/problem_definer.yaml +64 -0
  28. schematize/configs/prompt/en/tax/problem_definer_helper.yaml +100 -0
  29. schematize/configs/prompt/en/tax/query_generator.yaml +40 -0
  30. schematize/configs/prompt/en/tax/schema_assessment.yaml +78 -0
  31. schematize/configs/prompt/en/tax/schema_data_assessment.yaml +72 -0
  32. schematize/configs/prompt/en/tax/schema_data_assessment_merger.yaml +94 -0
  33. schematize/configs/prompt/en/tax/schema_data_refiner.yaml +126 -0
  34. schematize/configs/prompt/en/tax/schema_generator.yaml +66 -0
  35. schematize/configs/prompt/en/tax/schema_refiner.yaml +90 -0
  36. schematize/configs/prompt/eval/annotator_agreement.yaml +31 -0
  37. schematize/configs/prompt/eval/schema_evaluator.yaml +40 -0
  38. schematize/configs/prompt/pl/law/init_chat.yaml +133 -0
  39. schematize/configs/prompt/pl/law/problem_definer.yaml +52 -0
  40. schematize/configs/prompt/pl/law/problem_definer_helper.yaml +158 -0
  41. schematize/configs/prompt/pl/law/query_generator.yaml +32 -0
  42. schematize/configs/prompt/pl/law/schema_assessment.yaml +67 -0
  43. schematize/configs/prompt/pl/law/schema_data_assessment.yaml +70 -0
  44. schematize/configs/prompt/pl/law/schema_data_assessment_merger.yaml +90 -0
  45. schematize/configs/prompt/pl/law/schema_data_refiner.yaml +120 -0
  46. schematize/configs/prompt/pl/law/schema_generator.yaml +65 -0
  47. schematize/configs/prompt/pl/law/schema_refiner.yaml +88 -0
  48. schematize/configs/prompt/pl/tax/init_chat.yaml +126 -0
  49. schematize/configs/prompt/pl/tax/problem_definer.yaml +62 -0
  50. schematize/configs/prompt/pl/tax/problem_definer_helper.yaml +156 -0
  51. schematize/configs/prompt/pl/tax/query_generator.yaml +40 -0
  52. schematize/configs/prompt/pl/tax/schema_assessment.yaml +68 -0
  53. schematize/configs/prompt/pl/tax/schema_data_assessment.yaml +71 -0
  54. schematize/configs/prompt/pl/tax/schema_data_assessment_merger.yaml +93 -0
  55. schematize/configs/prompt/pl/tax/schema_data_refiner.yaml +121 -0
  56. schematize/configs/prompt/pl/tax/schema_generator.yaml +61 -0
  57. schematize/configs/prompt/pl/tax/schema_refiner.yaml +90 -0
  58. schematize/eval/__init__.py +0 -0
  59. schematize/eval/evaluator.py +218 -0
  60. schematize/py.typed +0 -0
  61. schematize/retrieval/__init__.py +3 -0
  62. schematize/retrieval/base.py +13 -0
  63. schematize/retrieval/huggingface.py +232 -0
  64. schematize/retrieval/weaviate.py +125 -0
  65. schematize/schema/__init__.py +0 -0
  66. schematize/schema/model.py +74 -0
  67. schematize/settings.py +8 -0
  68. schematize/utils/__init__.py +0 -0
  69. schematize/utils/langchain.py +10 -0
  70. schematize/utils/load.py +25 -0
  71. schematize/utils/retry.py +70 -0
  72. schematize-0.1.0.dist-info/METADATA +307 -0
  73. schematize-0.1.0.dist-info/RECORD +76 -0
  74. schematize-0.1.0.dist-info/WHEEL +4 -0
  75. schematize-0.1.0.dist-info/entry_points.txt +5 -0
  76. schematize-0.1.0.dist-info/licenses/LICENSE +21 -0
schematize/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ """schematize — agentic extraction-schema generation."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from schematize.agents.agent_state import AgentState, agent_state_to_json
6
+ from schematize.agents.schema_generator import SchemaGenerator, SchemaGeneratorPrompts
7
+ from schematize.eval.evaluator import SchemaEvaluator
8
+ from schematize.retrieval.base import DocumentRetriever
9
+ from schematize.schema.model import (
10
+ DynamicModelFactory,
11
+ FieldDef,
12
+ FieldType,
13
+ NamedFieldDef,
14
+ SchemaFields,
15
+ )
16
+ from schematize.utils.load import load_prompts
17
+
18
+ __all__ = [
19
+ "__version__",
20
+ "SchemaGenerator",
21
+ "SchemaGeneratorPrompts",
22
+ "SchemaEvaluator",
23
+ "DocumentRetriever",
24
+ "SchemaFields",
25
+ "FieldDef",
26
+ "NamedFieldDef",
27
+ "FieldType",
28
+ "DynamicModelFactory",
29
+ "AgentState",
30
+ "agent_state_to_json",
31
+ "load_prompts",
32
+ ]
File without changes
@@ -0,0 +1,100 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ from itertools import permutations
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import hydra
9
+ import yaml
10
+ from langchain_openai import ChatOpenAI
11
+ from omegaconf import DictConfig
12
+ from tqdm.asyncio import tqdm as atqdm
13
+
14
+ from schematize.eval.evaluator import AnnotatorAgreementEvaluator
15
+ from schematize.settings import PROMPTS_PATH
16
+ from schematize.utils.langchain import setup_langchain_llm_cache
17
+
18
+ _CONFIG_PATH = str(Path(__file__).parent / "../../../config")
19
+
20
+
21
+ @hydra.main(version_base=None, config_path=_CONFIG_PATH, config_name="evaluate_agreement")
22
+ def main(cfg: DictConfig) -> None:
23
+ logging.getLogger("httpx").setLevel(logging.WARNING)
24
+
25
+ if cfg.llm_cache:
26
+ setup_langchain_llm_cache()
27
+
28
+ llm = ChatOpenAI(
29
+ model=cfg.model_name,
30
+ base_url=cfg.api_url,
31
+ api_key=cfg.api_key,
32
+ temperature=cfg.llm.temperature,
33
+ max_tokens=cfg.llm.max_tokens,
34
+ use_responses_api=False,
35
+ **cfg.llm.model_kwargs,
36
+ )
37
+
38
+ with open(PROMPTS_PATH / "eval" / "annotator_agreement.yaml", "r") as f:
39
+ prompts = yaml.safe_load(f)
40
+
41
+ evaluator = AnnotatorAgreementEvaluator(llm, prompts["annotator_agreement_prompt"])
42
+
43
+ case_dir = Path(cfg.case_dir)
44
+ if not case_dir.exists():
45
+ raise ValueError(f"Case directory not found at {case_dir}")
46
+
47
+ expert_files = sorted(case_dir.glob("expert_*.yaml"))
48
+ assert len(expert_files) >= 2, f"Need at least 2 expert files in {case_dir}"
49
+
50
+ results = asyncio.run(_evaluate_all_pairs(evaluator, case_dir, expert_files, cfg))
51
+
52
+ output_path = Path(cfg.output) / "agreement.json"
53
+ output_path.parent.mkdir(parents=True, exist_ok=True)
54
+ with open(output_path, "w") as f:
55
+ json.dump(results, f, indent=2)
56
+ print(f"Annotator agreement results saved to {output_path}")
57
+
58
+
59
+ async def _evaluate_all_pairs(
60
+ evaluator: AnnotatorAgreementEvaluator,
61
+ case_dir: Path,
62
+ expert_files: list[Path],
63
+ cfg: DictConfig,
64
+ ) -> dict[str, Any]:
65
+ semaphore = asyncio.Semaphore(cfg.get("max_concurrent", 10))
66
+
67
+ async def eval_pair(target_file: Path, reference_file: Path) -> dict:
68
+ target_questions = _load_expert_questions(target_file)
69
+ reference_questions = _load_expert_questions(reference_file)
70
+ async with semaphore:
71
+ evaluation = await evaluator.aevaluate_agreement(target_questions, reference_questions)
72
+ return {
73
+ "target": target_file.stem,
74
+ "reference": reference_file.stem,
75
+ "total_questions": evaluation.total_questions,
76
+ "covered_questions": evaluation.covered_questions,
77
+ "high_confidence": evaluation.high_confidence_coverage,
78
+ "medium_confidence": evaluation.medium_confidence_coverage,
79
+ "low_confidence": evaluation.low_confidence_coverage,
80
+ }
81
+
82
+ pairs = await atqdm.gather(
83
+ *[eval_pair(target, reference) for target, reference in permutations(expert_files, 2)],
84
+ desc="Expert pairs",
85
+ )
86
+
87
+ return {
88
+ "case_dir": str(case_dir),
89
+ "num_experts": len(expert_files),
90
+ "pairs": pairs,
91
+ }
92
+
93
+
94
+ def _load_expert_questions(expert_path: Path) -> list[str]:
95
+ with open(expert_path, "r") as f:
96
+ return yaml.safe_load(f).get("questions", [])
97
+
98
+
99
+ if __name__ == "__main__":
100
+ main()
@@ -0,0 +1,169 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import hydra
8
+ import yaml
9
+ from langchain_openai import ChatOpenAI
10
+ from omegaconf import DictConfig
11
+ from tqdm.asyncio import tqdm as atqdm
12
+
13
+ from schematize.eval.evaluator import SchemaEvaluator
14
+ from schematize.settings import PROMPTS_PATH
15
+ from schematize.utils.langchain import setup_langchain_llm_cache
16
+
17
+ _CONFIG_PATH = str(Path(__file__).parent / "../../../config")
18
+
19
+
20
+ @hydra.main(version_base=None, config_path=_CONFIG_PATH, config_name="evaluate")
21
+ def main(cfg: DictConfig) -> None:
22
+ logging.getLogger("httpx").setLevel(logging.WARNING)
23
+
24
+ if cfg.llm_cache:
25
+ setup_langchain_llm_cache()
26
+
27
+ llm = ChatOpenAI(
28
+ model=cfg.model_name,
29
+ base_url=cfg.api_url,
30
+ api_key=cfg.api_key,
31
+ temperature=cfg.llm.temperature,
32
+ max_tokens=cfg.llm.max_tokens,
33
+ use_responses_api=False,
34
+ **cfg.llm.model_kwargs,
35
+ )
36
+
37
+ with open(PROMPTS_PATH / "eval" / "schema_evaluator.yaml", "r") as f:
38
+ prompts = yaml.safe_load(f)
39
+
40
+ evaluator = SchemaEvaluator(llm, prompts["schema_evaluator_prompt"])
41
+
42
+ state_paths = _get_state_paths(Path(cfg.state_dir))
43
+ case_dir = Path(cfg.case_dir)
44
+
45
+ if not case_dir.exists():
46
+ raise ValueError(f"Case directory not found at {case_dir}")
47
+
48
+ expert_files = sorted(case_dir.glob("expert_*.yaml"))
49
+ assert len(expert_files) > 0, f"No expert files found in {case_dir}"
50
+
51
+ asyncio.run(_run_all(evaluator, state_paths, case_dir, expert_files, cfg))
52
+
53
+
54
+ async def _run_all(
55
+ evaluator: SchemaEvaluator,
56
+ state_paths: list[Path],
57
+ case_dir: Path,
58
+ expert_files: list[Path],
59
+ cfg: DictConfig,
60
+ ) -> None:
61
+ semaphore = asyncio.Semaphore(cfg.get("max_concurrent", 10))
62
+ await atqdm.gather(
63
+ *[
64
+ _evaluate_and_save(evaluator, p, case_dir, expert_files, cfg, semaphore)
65
+ for p in state_paths
66
+ ],
67
+ desc="States",
68
+ )
69
+
70
+
71
+ async def _evaluate_and_save(
72
+ evaluator: SchemaEvaluator,
73
+ state_path: Path,
74
+ case_dir: Path,
75
+ expert_files: list[Path],
76
+ cfg: DictConfig,
77
+ semaphore: asyncio.Semaphore,
78
+ ) -> None:
79
+ state = _load_state(state_path)
80
+ if cfg.final_only:
81
+ schema_history = [state["current_schema"]] if state.get("current_schema") else []
82
+ else:
83
+ schema_history = state.get("schema_history", [])
84
+ if state.get("current_schema"):
85
+ schema_history.append(state["current_schema"])
86
+
87
+ results = await _evaluate(evaluator, state_path, case_dir, schema_history, expert_files, semaphore)
88
+
89
+ output_path = (
90
+ Path(cfg.output) / state_path.relative_to(cfg.state_dir).parent / "evaluation.json"
91
+ )
92
+ output_path.parent.mkdir(parents=True, exist_ok=True)
93
+ with open(output_path, "w") as f:
94
+ json.dump(results, f, indent=2)
95
+ print(f"Evaluation results saved to {output_path}")
96
+
97
+
98
+ async def _evaluate(
99
+ evaluator: SchemaEvaluator,
100
+ state_path: Path,
101
+ case_dir: Path,
102
+ schema_history: list[dict],
103
+ expert_files: list[Path],
104
+ semaphore: asyncio.Semaphore,
105
+ ) -> dict[str, Any]:
106
+ schema_results: dict[int, dict] = {
107
+ i: {"schema_index": i, "num_fields": len(schema), "experts": []}
108
+ for i, schema in enumerate(schema_history)
109
+ }
110
+
111
+ async def eval_one(schema_idx: int, schema: dict, expert_file: Path) -> tuple:
112
+ questions = _load_expert_questions(expert_file)
113
+ async with semaphore:
114
+ evaluation = await evaluator.aevaluate_schema(schema, questions)
115
+ return schema_idx, expert_file.stem, evaluation
116
+
117
+ tasks = [
118
+ eval_one(schema_idx, schema, expert_file)
119
+ for schema_idx, schema in enumerate(schema_history)
120
+ for expert_file in expert_files
121
+ ]
122
+
123
+ for coro in asyncio.as_completed(tasks):
124
+ schema_idx, expert_stem, evaluation = await coro
125
+ schema_results[schema_idx]["experts"].append(
126
+ {
127
+ "expert": expert_stem,
128
+ "total_questions": evaluation.total_questions,
129
+ "covered_questions": evaluation.covered_questions,
130
+ "high_confidence": evaluation.high_confidence_coverage,
131
+ "medium_confidence": evaluation.medium_confidence_coverage,
132
+ "low_confidence": evaluation.low_confidence_coverage,
133
+ }
134
+ )
135
+
136
+ return {
137
+ "state_path": str(state_path),
138
+ "case_dir": str(case_dir),
139
+ "num_schemas": len(schema_history),
140
+ "num_experts": len(expert_files),
141
+ "evaluations": [schema_results[i] for i in range(len(schema_history))],
142
+ }
143
+
144
+
145
+ def _load_state(state_path: Path) -> dict:
146
+ with open(state_path, "r") as f:
147
+ return json.load(f)
148
+
149
+
150
+ def _load_expert_questions(expert_path: Path) -> list[str]:
151
+ with open(expert_path, "r") as f:
152
+ return yaml.safe_load(f).get("questions", [])
153
+
154
+
155
+ def _get_state_paths(state_dir: Path) -> list[Path]:
156
+ direct = state_dir / "state.json"
157
+ if direct.exists():
158
+ return [direct]
159
+ result = [
160
+ p for p in state_dir.glob("*/state.json")
161
+ if not (p.parent / "evaluation.json").exists()
162
+ ]
163
+ if not result:
164
+ raise ValueError(f"No unevaluated state files found in {state_dir}")
165
+ return result
166
+
167
+
168
+ if __name__ == "__main__":
169
+ main()
@@ -0,0 +1,116 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Annotated, Optional
4
+
5
+ import typer
6
+ from dotenv import load_dotenv
7
+ from langchain_openai import ChatOpenAI
8
+
9
+ from schematize.agents.agent_state import agent_state_to_json
10
+ from schematize.agents.schema_generator import SchemaGenerator, SchemaGeneratorPrompts
11
+ from schematize.utils.load import load_prompts
12
+
13
+ app = typer.Typer()
14
+
15
+
16
+ @app.command()
17
+ def main(
18
+ model: Annotated[str, typer.Option()] = "gpt-4o-mini",
19
+ language: Annotated[str, typer.Option(help="pl or en")] = "en",
20
+ system_type: Annotated[str, typer.Option(help="tax or law")] = "tax",
21
+ temperature: Annotated[float, typer.Option()] = 0.2,
22
+ max_tokens: Annotated[int, typer.Option()] = 32_000,
23
+ reasoning_effort: Annotated[Optional[str], typer.Option(help="low | medium | high")] = None,
24
+ output: Annotated[Path, typer.Option()] = Path("state.json"),
25
+ api_url: Annotated[Optional[str], typer.Option()] = None,
26
+ api_key: Annotated[Optional[str], typer.Option()] = None,
27
+ retriever_type: Annotated[str, typer.Option(help="weaviate | mmlw | huggingface")] = "weaviate",
28
+ dataset: Annotated[Optional[str], typer.Option(help="HuggingFace dataset identifier")] = None,
29
+ text_column: Annotated[str, typer.Option(help="Dataset column to embed and search")] = "text",
30
+ max_documents: Annotated[Optional[int], typer.Option(help="Max rows to index (None = all)")] = None,
31
+ index_path: Annotated[Optional[str], typer.Option(help="Directory to cache the FAISS index")] = None,
32
+ collection_name: Annotated[Optional[str], typer.Option(help="Weaviate collection name")] = None,
33
+ target_vector: Annotated[Optional[str], typer.Option(help="Weaviate named vector")] = None,
34
+ wv_filter: Annotated[Optional[list[str]], typer.Option(help="Weaviate filters as key=value")] = None,
35
+ max_refinement_rounds: Annotated[int, typer.Option()] = 3,
36
+ min_refinement_rounds: Annotated[int, typer.Option()] = 2,
37
+ max_data_refinement_rounds: Annotated[int, typer.Option()] = 3,
38
+ min_data_refinement_rounds: Annotated[int, typer.Option()] = 2,
39
+ data_assessment_top_k: Annotated[int, typer.Option()] = 50,
40
+ data_assessment_num_examples: Annotated[int, typer.Option()] = 3,
41
+ data_assessment_random_seed: Annotated[int, typer.Option()] = 17,
42
+ data_assessment_document_max_chars: Annotated[int, typer.Option()] = 32_000,
43
+ ) -> None:
44
+ load_dotenv(".env")
45
+
46
+ if language not in ("pl", "en"):
47
+ raise typer.BadParameter(f"language must be 'pl' or 'en', got '{language}'")
48
+ if system_type not in ("tax", "law"):
49
+ raise typer.BadParameter(f"system_type must be 'tax' or 'law', got '{system_type}'")
50
+
51
+ prompts = load_prompts(language, system_type)
52
+
53
+ if retriever_type == "weaviate":
54
+ try:
55
+ from schematize.retrieval.weaviate import WeaviateRetriever
56
+ except ImportError:
57
+ raise typer.Exit("Weaviate retriever requires: pip install schematize[weaviate]")
58
+ if not collection_name:
59
+ raise typer.BadParameter("--collection-name is required for weaviate retriever")
60
+ filters = dict(f.split("=", 1) for f in (wv_filter or []))
61
+ retriever = WeaviateRetriever(
62
+ collection_name=collection_name,
63
+ target_vector=target_vector,
64
+ filters=filters,
65
+ )
66
+ else:
67
+ try:
68
+ from schematize.retrieval.huggingface import (
69
+ HuggingFaceRetriever,
70
+ MMLWRobertaV2Retriever,
71
+ )
72
+ except ImportError:
73
+ raise typer.Exit("HuggingFace retriever requires: pip install schematize[huggingface]")
74
+ if not dataset:
75
+ raise typer.BadParameter("--dataset is required for huggingface/mmlw retriever")
76
+ kwargs = dict(dataset_name=dataset, text_column=text_column, max_documents=max_documents, index_path=index_path)
77
+ retriever = MMLWRobertaV2Retriever(**kwargs) if retriever_type == "mmlw" else HuggingFaceRetriever(**kwargs)
78
+
79
+ llm = ChatOpenAI(
80
+ model=model,
81
+ base_url=api_url or os.getenv("API_URL"),
82
+ api_key=api_key or os.getenv("API_KEY"),
83
+ temperature=temperature,
84
+ max_tokens=max_tokens,
85
+ use_responses_api=False,
86
+ reasoning_effort=reasoning_effort,
87
+ )
88
+
89
+ schema_system = SchemaGenerator(
90
+ llm,
91
+ retriever,
92
+ SchemaGeneratorPrompts(**prompts),
93
+ max_refinement_rounds=max_refinement_rounds,
94
+ min_refinement_rounds=min_refinement_rounds,
95
+ max_data_refinement_rounds=max_data_refinement_rounds,
96
+ min_data_refinement_rounds=min_data_refinement_rounds,
97
+ data_assessment_top_k=data_assessment_top_k,
98
+ data_assessment_num_examples=data_assessment_num_examples,
99
+ data_assessment_random_seed=data_assessment_random_seed,
100
+ data_assessment_document_max_chars=data_assessment_document_max_chars,
101
+ recursion_limit=100,
102
+ )
103
+
104
+ typer.echo("Describe the schema to generate:")
105
+ input_text = input()
106
+
107
+ final_state = schema_system.stream_graph_updates(input_text)
108
+
109
+ with output.open("w") as f:
110
+ f.write(agent_state_to_json(final_state))
111
+
112
+ typer.echo(f"State saved to {output}")
113
+
114
+
115
+ if __name__ == "__main__":
116
+ app()
@@ -0,0 +1,151 @@
1
+ import logging
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import hydra
7
+ import yaml
8
+ from dotenv import load_dotenv
9
+ from langchain_core.messages import AIMessage, HumanMessage
10
+ from langchain_openai import ChatOpenAI
11
+ from loguru import logger
12
+ from omegaconf import DictConfig
13
+
14
+ from schematize.agents.agent_state import AgentState, agent_state_to_json
15
+ from schematize.agents.schema_generator import SchemaGenerator, SchemaGeneratorPrompts
16
+ from schematize.utils.langchain import setup_langchain_llm_cache
17
+ from schematize.utils.load import load_prompts
18
+
19
+ _CONFIG_PATH = str(Path(__file__).parent / "../../../config")
20
+
21
+ load_dotenv()
22
+
23
+ logger.remove()
24
+ logger.add(sys.stderr, level="INFO")
25
+ logging.getLogger("httpx").setLevel(logging.WARNING)
26
+
27
+
28
+ @hydra.main(version_base=None, config_path=_CONFIG_PATH, config_name="ablation")
29
+ def main(cfg: DictConfig) -> None:
30
+ assert cfg.case.language in ["pl", "en"], "Invalid language"
31
+ assert cfg.case.system_type in ["law", "tax"], "Invalid system type"
32
+
33
+ if cfg.llm_cache:
34
+ setup_langchain_llm_cache()
35
+
36
+ prompts = load_prompts(cfg.case.language, cfg.case.system_type)
37
+ cases = _load_cases(Path(cfg.cases_path))
38
+
39
+ case_name = cfg.case.name
40
+ assert case_name in cases, f"Case '{case_name}' not found. Available: {list(cases.keys())}"
41
+ case_data = cases[case_name]
42
+
43
+ retriever = _build_retriever(cfg.retriever)
44
+
45
+ llm = ChatOpenAI(
46
+ model=cfg.model_name,
47
+ base_url=cfg.api_url,
48
+ api_key=cfg.api_key,
49
+ temperature=cfg.llm.temperature,
50
+ max_tokens=cfg.llm.max_tokens,
51
+ use_responses_api=False,
52
+ **cfg.llm.model_kwargs,
53
+ )
54
+
55
+ sg_cfg = cfg.schema_generator
56
+ schema_system = SchemaGenerator(
57
+ llm,
58
+ retriever,
59
+ SchemaGeneratorPrompts(**prompts),
60
+ max_refinement_rounds=sg_cfg.max_refinement_rounds,
61
+ min_refinement_rounds=sg_cfg.min_refinement_rounds,
62
+ max_data_refinement_rounds=sg_cfg.max_data_refinement_rounds,
63
+ min_data_refinement_rounds=sg_cfg.min_data_refinement_rounds,
64
+ data_assessment_top_k=sg_cfg.data_assessment_top_k,
65
+ data_assessment_num_examples=sg_cfg.data_assessment_num_examples,
66
+ data_assessment_random_seed=sg_cfg.data_assessment_random_seed,
67
+ data_assessment_document_max_chars=sg_cfg.data_assessment_document_max_chars,
68
+ skip_problem_definition=sg_cfg.skip_problem_definition,
69
+ skip_refinement=sg_cfg.skip_refinement,
70
+ skip_data_grounded=sg_cfg.skip_data_grounded,
71
+ recursion_limit=100,
72
+ )
73
+
74
+ if not sg_cfg.skip_problem_definition:
75
+ if "problem_help" in case_data:
76
+ schema_system.problem_definer_helper = _mock_problem_helper(case_data["problem_help"])
77
+ if "user_feedback" in case_data:
78
+ schema_system.human_feedback = _mock_user_feedback(case_data["user_feedback"])
79
+ if "human_message" in case_data:
80
+ schema_system.human_message = _mock_human_message(case_data["human_message"])
81
+
82
+ schema_system.graph = schema_system.build_graph()
83
+
84
+ user_input = case_data.get("user_input", "Generate a schema for legal documents")
85
+ final_state = schema_system.stream_graph_updates(user_input)
86
+
87
+ output_path = Path(cfg.output) / "state.json"
88
+ output_path.parent.mkdir(parents=True, exist_ok=True)
89
+ with output_path.open("w") as f:
90
+ f.write(agent_state_to_json(final_state))
91
+ logger.info("State saved to {}", output_path)
92
+
93
+ schema_path = Path(cfg.output) / "schema.json"
94
+ schema_path.write_text(final_state.get("current_schema").model_dump_json(indent=2))
95
+ logger.info("Schema saved to {}", schema_path)
96
+
97
+
98
+ def _build_retriever(r_cfg):
99
+ if r_cfg.type == "weaviate":
100
+ try:
101
+ from schematize.retrieval.weaviate import WeaviateRetriever
102
+ except ImportError:
103
+ raise SystemExit("Weaviate retriever requires: pip install schematize[weaviate]")
104
+ filters = dict(r_cfg.wv_filters) if r_cfg.wv_filters else {}
105
+ return WeaviateRetriever(
106
+ collection_name=r_cfg.collection_name,
107
+ target_vector=r_cfg.target_vector or None,
108
+ filters=filters,
109
+ )
110
+ try:
111
+ from schematize.retrieval.huggingface import HuggingFaceRetriever, MMLWRobertaV2Retriever
112
+ except ImportError:
113
+ raise SystemExit("HuggingFace retriever requires: pip install schematize[huggingface]")
114
+ kwargs = dict(
115
+ dataset_name=r_cfg.dataset_name,
116
+ text_column=r_cfg.text_column,
117
+ split=r_cfg.split,
118
+ max_documents=r_cfg.max_documents,
119
+ index_path=r_cfg.index_path,
120
+ )
121
+ return MMLWRobertaV2Retriever(**kwargs) if r_cfg.type == "mmlw" else HuggingFaceRetriever(**kwargs)
122
+
123
+
124
+ def _load_cases(cases_path: Path) -> dict[str, dict[str, str]]:
125
+ assert cases_path.exists(), f"Cases directory does not exist: {cases_path}"
126
+ return {p.stem: yaml.safe_load(p.read_text()) for p in cases_path.glob("*.yaml")}
127
+
128
+
129
+ def _mock_problem_helper(text: str):
130
+ def _fn(state: AgentState) -> dict[str, Any]:
131
+ return {"messages": [AIMessage(content=text)], "problem_help": text}
132
+ return _fn
133
+
134
+
135
+ def _mock_user_feedback(text: str):
136
+ def _fn(state: AgentState) -> dict[str, Any]:
137
+ return {"user_feedback": text, "messages": [HumanMessage(content=text)]}
138
+ return _fn
139
+
140
+
141
+ def _mock_human_message(text: str):
142
+ def _fn(state: AgentState) -> dict[str, Any]:
143
+ return {
144
+ "messages": [HumanMessage(content=text)],
145
+ "final_messages": [HumanMessage(content=text)],
146
+ }
147
+ return _fn
148
+
149
+
150
+ if __name__ == "__main__":
151
+ main()