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.
- schematize/__init__.py +32 -0
- schematize/_scripts/__init__.py +0 -0
- schematize/_scripts/evaluate_annotator_agreement.py +100 -0
- schematize/_scripts/evaluate_schema.py +169 -0
- schematize/_scripts/schema_generator.py +116 -0
- schematize/_scripts/schema_generator_ablation.py +151 -0
- schematize/_scripts/schema_generator_mocked.py +153 -0
- schematize/agents/__init__.py +0 -0
- schematize/agents/agent_state.py +54 -0
- schematize/agents/basic_agents.py +115 -0
- schematize/agents/chat_agent.py +85 -0
- schematize/agents/data_agents.py +165 -0
- schematize/agents/output_models.py +50 -0
- schematize/agents/schema_generator.py +339 -0
- schematize/configs/__init__.py +0 -0
- schematize/configs/prompt/en/law/init_chat.yaml +137 -0
- schematize/configs/prompt/en/law/problem_definer.yaml +52 -0
- schematize/configs/prompt/en/law/problem_definer_helper.yaml +119 -0
- schematize/configs/prompt/en/law/query_generator.yaml +32 -0
- schematize/configs/prompt/en/law/schema_assessment.yaml +77 -0
- schematize/configs/prompt/en/law/schema_data_assessment.yaml +71 -0
- schematize/configs/prompt/en/law/schema_data_assessment_merger.yaml +93 -0
- schematize/configs/prompt/en/law/schema_data_refiner.yaml +125 -0
- schematize/configs/prompt/en/law/schema_generator.yaml +65 -0
- schematize/configs/prompt/en/law/schema_refiner.yaml +88 -0
- schematize/configs/prompt/en/tax/init_chat.yaml +128 -0
- schematize/configs/prompt/en/tax/problem_definer.yaml +64 -0
- schematize/configs/prompt/en/tax/problem_definer_helper.yaml +100 -0
- schematize/configs/prompt/en/tax/query_generator.yaml +40 -0
- schematize/configs/prompt/en/tax/schema_assessment.yaml +78 -0
- schematize/configs/prompt/en/tax/schema_data_assessment.yaml +72 -0
- schematize/configs/prompt/en/tax/schema_data_assessment_merger.yaml +94 -0
- schematize/configs/prompt/en/tax/schema_data_refiner.yaml +126 -0
- schematize/configs/prompt/en/tax/schema_generator.yaml +66 -0
- schematize/configs/prompt/en/tax/schema_refiner.yaml +90 -0
- schematize/configs/prompt/eval/annotator_agreement.yaml +31 -0
- schematize/configs/prompt/eval/schema_evaluator.yaml +40 -0
- schematize/configs/prompt/pl/law/init_chat.yaml +133 -0
- schematize/configs/prompt/pl/law/problem_definer.yaml +52 -0
- schematize/configs/prompt/pl/law/problem_definer_helper.yaml +158 -0
- schematize/configs/prompt/pl/law/query_generator.yaml +32 -0
- schematize/configs/prompt/pl/law/schema_assessment.yaml +67 -0
- schematize/configs/prompt/pl/law/schema_data_assessment.yaml +70 -0
- schematize/configs/prompt/pl/law/schema_data_assessment_merger.yaml +90 -0
- schematize/configs/prompt/pl/law/schema_data_refiner.yaml +120 -0
- schematize/configs/prompt/pl/law/schema_generator.yaml +65 -0
- schematize/configs/prompt/pl/law/schema_refiner.yaml +88 -0
- schematize/configs/prompt/pl/tax/init_chat.yaml +126 -0
- schematize/configs/prompt/pl/tax/problem_definer.yaml +62 -0
- schematize/configs/prompt/pl/tax/problem_definer_helper.yaml +156 -0
- schematize/configs/prompt/pl/tax/query_generator.yaml +40 -0
- schematize/configs/prompt/pl/tax/schema_assessment.yaml +68 -0
- schematize/configs/prompt/pl/tax/schema_data_assessment.yaml +71 -0
- schematize/configs/prompt/pl/tax/schema_data_assessment_merger.yaml +93 -0
- schematize/configs/prompt/pl/tax/schema_data_refiner.yaml +121 -0
- schematize/configs/prompt/pl/tax/schema_generator.yaml +61 -0
- schematize/configs/prompt/pl/tax/schema_refiner.yaml +90 -0
- schematize/eval/__init__.py +0 -0
- schematize/eval/evaluator.py +218 -0
- schematize/py.typed +0 -0
- schematize/retrieval/__init__.py +3 -0
- schematize/retrieval/base.py +13 -0
- schematize/retrieval/huggingface.py +232 -0
- schematize/retrieval/weaviate.py +125 -0
- schematize/schema/__init__.py +0 -0
- schematize/schema/model.py +74 -0
- schematize/settings.py +8 -0
- schematize/utils/__init__.py +0 -0
- schematize/utils/langchain.py +10 -0
- schematize/utils/load.py +25 -0
- schematize/utils/retry.py +70 -0
- schematize-0.1.0.dist-info/METADATA +307 -0
- schematize-0.1.0.dist-info/RECORD +76 -0
- schematize-0.1.0.dist-info/WHEEL +4 -0
- schematize-0.1.0.dist-info/entry_points.txt +5 -0
- 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()
|