elaws-parser 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.
- elaws_parser/__init__.py +19 -0
- elaws_parser/hourei_apiv2.py +128 -0
- elaws_parser/law_extraction.py +484 -0
- elaws_parser/law_extraction_v2.py +739 -0
- elaws_parser/text_converter.py +407 -0
- elaws_parser/yaml_converter.py +727 -0
- elaws_parser-0.1.0.dist-info/METADATA +149 -0
- elaws_parser-0.1.0.dist-info/RECORD +10 -0
- elaws_parser-0.1.0.dist-info/WHEEL +5 -0
- elaws_parser-0.1.0.dist-info/top_level.txt +1 -0
elaws_parser/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .hourei_apiv2 import (
|
|
2
|
+
get_lawid_from_lawtitle,
|
|
3
|
+
get_lawdata_from_law_id,
|
|
4
|
+
get_lawdata_from_lawname,
|
|
5
|
+
save_xml_string_to_file,
|
|
6
|
+
extract_sections_from_xml,
|
|
7
|
+
)
|
|
8
|
+
from .text_converter import convert_xml_to_text, LawXmlParser
|
|
9
|
+
from .yaml_converter import convert_xml_to_yaml, LawToYamlConverter
|
|
10
|
+
|
|
11
|
+
# LLM/LangGraph機能はオプショナル依存関係のため、インストールされていない場合は無視する
|
|
12
|
+
try:
|
|
13
|
+
from .law_extraction_v2 import (
|
|
14
|
+
LegalExtractionConfig,
|
|
15
|
+
create_legal_extraction_system,
|
|
16
|
+
YamlArticleExtractor,
|
|
17
|
+
)
|
|
18
|
+
except ImportError:
|
|
19
|
+
pass
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
eGovのAPI v2を利用して,法令を取得するコード
|
|
3
|
+
取得した法令のxml構造を解析して,必要な情報を返す.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# TODO :: パーサーは,textのパーサーとyamlのパーサー
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from typing import Dict, Literal
|
|
12
|
+
from xml.etree import ElementTree
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@lru_cache
|
|
18
|
+
def get_lawid_from_lawtitle(
|
|
19
|
+
law_title: str, *, if_exact: bool = True
|
|
20
|
+
) -> str | Dict[str, str]:
|
|
21
|
+
"""APIから法令タイトルでヒットする法令IDを取得(完全一致のみ)"""
|
|
22
|
+
url = "https://laws.e-gov.go.jp/api/2/laws"
|
|
23
|
+
r = requests.get(url, params={"response_format": "xml", "law_title": law_title})
|
|
24
|
+
# XMLデータの解析
|
|
25
|
+
root = ElementTree.fromstring(r.content.decode(encoding="utf-8"))
|
|
26
|
+
|
|
27
|
+
laws_elem = root.find("laws")
|
|
28
|
+
if laws_elem is None:
|
|
29
|
+
print("Error: 'laws' element not found in response.")
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
counter = 0
|
|
33
|
+
law_dict = {} # 辞書{名称: 法令番号}の作成
|
|
34
|
+
for law in laws_elem.findall("law"): # loop over <law> elements
|
|
35
|
+
counter += 1
|
|
36
|
+
|
|
37
|
+
law_info = law.find("law_info")
|
|
38
|
+
revision_info = law.find("revision_info")
|
|
39
|
+
|
|
40
|
+
if law_info is None or revision_info is None:
|
|
41
|
+
continue # skip incomplete entries
|
|
42
|
+
|
|
43
|
+
law_id: str = law_info.findtext("law_id", default="(no id)")
|
|
44
|
+
law_num: str = law_info.findtext("law_num", default="(no number)")
|
|
45
|
+
lawtitle: str = revision_info.findtext("law_title", default="(no title)")
|
|
46
|
+
|
|
47
|
+
print(f"ID: {law_id}, Num: {law_num}, Title: {lawtitle}")
|
|
48
|
+
law_dict[lawtitle] = law_id
|
|
49
|
+
print(f"Number of laws: {counter}")
|
|
50
|
+
if if_exact:
|
|
51
|
+
return law_dict[law_title] # allow exact match
|
|
52
|
+
return law_dict # return all matches
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_lawdata_from_law_id(law_id: str, output_type: Literal["xml", "list"]):
|
|
56
|
+
"""法令IDから法令データを取得"""
|
|
57
|
+
url = f"https://laws.e-gov.go.jp/api/2/law_data/{law_id}"
|
|
58
|
+
r = requests.get(url, params={"response_format": "xml"})
|
|
59
|
+
if r.status_code != 200:
|
|
60
|
+
print(f"Error fetching law data for ID {law_id}: {r.status_code}")
|
|
61
|
+
return None
|
|
62
|
+
if output_type == "xml":
|
|
63
|
+
return r.content.decode(encoding="utf-8")
|
|
64
|
+
|
|
65
|
+
if output_type == "list":
|
|
66
|
+
# XMLデータの解析
|
|
67
|
+
root = ElementTree.fromstring(r.content.decode(encoding="utf-8"))
|
|
68
|
+
contents = [e.text.strip() for e in root.iter() if e.text]
|
|
69
|
+
return [t for t in contents if t]
|
|
70
|
+
raise ValueError(f"Supported output type is xml or list. Got {output_type}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_lawdata_from_lawname(law_name: str) -> str:
|
|
74
|
+
"""法令名から法令データを取得(完全一致のみ)"""
|
|
75
|
+
law_id: str = get_lawid_from_lawtitle(law_name, if_exact=True)
|
|
76
|
+
law_text: str = get_lawdata_from_law_id(law_id, "xml")
|
|
77
|
+
return law_text
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def save_xml_string_to_file(xml_string: str, filename: str):
|
|
81
|
+
"""save xml string to a file"""
|
|
82
|
+
with open(filename, "w", encoding="utf-8") as f:
|
|
83
|
+
f.write(xml_string)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def extract_sections_from_xml(xml_string: str) -> Dict[str, str | None | list[str]]:
|
|
87
|
+
"""TOC, MainProvision,SupplProvisionの3つを取得"""
|
|
88
|
+
root = ElementTree.fromstring(xml_string)
|
|
89
|
+
|
|
90
|
+
# law_infoタグを取得
|
|
91
|
+
law_full_text = root.find("law_full_text")
|
|
92
|
+
if law_full_text is None:
|
|
93
|
+
raise ValueError("law_full_textタグが見つかりません")
|
|
94
|
+
|
|
95
|
+
# <Law> の中にある <LawBody> を探す
|
|
96
|
+
law = law_full_text.find("Law")
|
|
97
|
+
if law is None:
|
|
98
|
+
raise ValueError("<Law> タグが <law_full_text> 内に見つかりません")
|
|
99
|
+
|
|
100
|
+
law_body = law.find("LawBody")
|
|
101
|
+
if law_body is None:
|
|
102
|
+
raise ValueError("<LawBody> タグが <Law> 内に見つかりません")
|
|
103
|
+
|
|
104
|
+
# 対象の3つのタグを取得
|
|
105
|
+
toc = law_body.find("TOC")
|
|
106
|
+
main_prov = law_body.find("MainProvision")
|
|
107
|
+
suppl_provs = law_body.findall("SupplProvision")
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
"TOC": (
|
|
111
|
+
ElementTree.tostring(toc, encoding="unicode") if toc is not None else None
|
|
112
|
+
),
|
|
113
|
+
"MainProvision": (
|
|
114
|
+
ElementTree.tostring(main_prov, encoding="unicode")
|
|
115
|
+
if main_prov is not None
|
|
116
|
+
else None
|
|
117
|
+
),
|
|
118
|
+
# "SupplProvision": (
|
|
119
|
+
# ElementTree.tostring(suppl_prov, encoding="unicode")
|
|
120
|
+
# if suppl_prov is not None
|
|
121
|
+
# else None
|
|
122
|
+
# ),
|
|
123
|
+
"SupplProvision": (
|
|
124
|
+
[ElementTree.tostring(s, encoding="unicode") for s in suppl_provs]
|
|
125
|
+
if suppl_provs
|
|
126
|
+
else None
|
|
127
|
+
),
|
|
128
|
+
}
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Literal, Optional, TypedDict
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from langchain_core.language_models import BaseLLM
|
|
10
|
+
from langchain_core.messages import BaseMessage, SystemMessage
|
|
11
|
+
from langchain_core.prompts import PromptTemplate
|
|
12
|
+
from langchain_openai import ChatOpenAI
|
|
13
|
+
from langgraph.graph import END, StateGraph # CompiledGraph
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
# ロギング設定
|
|
17
|
+
logging.basicConfig(level=logging.INFO)
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProcessingStage(Enum):
|
|
22
|
+
"""4つの処理段階の定義.エラー処理等で利用"""
|
|
23
|
+
|
|
24
|
+
LAW_EXTRACTION = "law_extraction"
|
|
25
|
+
REGULATION_EXTRACTION = "regulation_extraction"
|
|
26
|
+
SUMMARY_GENERATION = "summary_generation"
|
|
27
|
+
COMPLETED = "completed"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ExtractionResult:
|
|
32
|
+
"""抽出結果のデータクラス"""
|
|
33
|
+
|
|
34
|
+
content: str
|
|
35
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
36
|
+
token_count: Optional[int] = None
|
|
37
|
+
processing_time: Optional[float] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class LegalDocument:
|
|
42
|
+
"""法令文書のデータクラス(YAML対応版)"""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
content: str # 法令全文
|
|
46
|
+
document_type: Literal["law", "regulation"] # 法令 or 施行規則
|
|
47
|
+
yaml_data: Optional[Dict[str, Any]] = None # YAML構造データを追加
|
|
48
|
+
articles: Optional[List[str]] = None
|
|
49
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
50
|
+
|
|
51
|
+
def __post_init__(self):
|
|
52
|
+
"""文書の基本検証"""
|
|
53
|
+
if not self.name or not self.content:
|
|
54
|
+
raise ValueError("文書名と内容は必須です")
|
|
55
|
+
|
|
56
|
+
# YAML構造データが提供されていない場合の警告
|
|
57
|
+
if self.yaml_data is None:
|
|
58
|
+
logger.warning(
|
|
59
|
+
f"{self.name}: YAML構造データが提供されていません。新しい抽出機能は使用できません。"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class GraphState(TypedDict):
|
|
64
|
+
"""グラフの状態管理用TypedDict"""
|
|
65
|
+
|
|
66
|
+
law_document: LegalDocument # 法令文書
|
|
67
|
+
regulation_document: LegalDocument # 施行規則文書
|
|
68
|
+
target_articles: List[str] # 抽出対象の条文リスト
|
|
69
|
+
extracted_law_article_numbers: Optional[List[int]] # 抽出された法令の条文
|
|
70
|
+
extracted_law_content: Optional[str] # 抽出された法令内容
|
|
71
|
+
extracted_regulation_content: Optional[str] # 抽出された施行規則内容
|
|
72
|
+
extracted_regulation_article_numbers: Optional[
|
|
73
|
+
List[int]
|
|
74
|
+
] # 抽出された施行規則の条項一覧
|
|
75
|
+
final_summary: Optional[str] # 最終的な要点
|
|
76
|
+
current_stage: ProcessingStage # 現在の処理段階
|
|
77
|
+
error_message: Optional[str] # エラーメッセージ
|
|
78
|
+
metadata: Dict[str, Any] # 処理メタデータ
|
|
79
|
+
application_item: Optional[str] # 申請項目
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def flatten_state(state: GraphState) -> dict[str, str]:
|
|
83
|
+
"""stateからプロンプト展開用のcontextを自動生成"""
|
|
84
|
+
context = {}
|
|
85
|
+
for key, value in state.items():
|
|
86
|
+
if hasattr(value, "name") and hasattr(value, "content"):
|
|
87
|
+
context[f"{key}_name"] = value.name
|
|
88
|
+
context[f"{key}_text"] = value.content
|
|
89
|
+
elif hasattr(value, "name"):
|
|
90
|
+
context[f"{key}_name"] = value.name
|
|
91
|
+
elif isinstance(value, (str, int, float)):
|
|
92
|
+
context[key] = str(value)
|
|
93
|
+
# 必要なら他の型にも対応
|
|
94
|
+
return context
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class PromptManager:
|
|
98
|
+
"""プロンプト管理クラス
|
|
99
|
+
TODO :: prompts_dirをここで管理して良いかどうか.
|
|
100
|
+
理想的には,load_promptでまとめてdir/fileを指定したい.
|
|
101
|
+
そのためには,各部でload_promptを呼び出している部分のファイル名をハードコードから外から与える形にする必要がある.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, prompts_dir: Path = Path("prompts")):
|
|
105
|
+
self.prompts_dir = prompts_dir
|
|
106
|
+
self._prompts_cache: Dict[str, PromptTemplate] = {}
|
|
107
|
+
|
|
108
|
+
def load_prompt(self, prompt_name: str) -> PromptTemplate:
|
|
109
|
+
"""プロンプトテンプレートの読み込み(キャッシュ機能付き)"""
|
|
110
|
+
if prompt_name in self._prompts_cache:
|
|
111
|
+
return self._prompts_cache[prompt_name]
|
|
112
|
+
|
|
113
|
+
prompt_path = self.prompts_dir / f"{prompt_name}.yaml"
|
|
114
|
+
if not prompt_path.exists():
|
|
115
|
+
raise FileNotFoundError(
|
|
116
|
+
f"プロンプトファイルが見つかりません: {prompt_path}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
with open(prompt_path, "r", encoding="utf-8") as f:
|
|
120
|
+
prompt_data = yaml.safe_load(f)
|
|
121
|
+
|
|
122
|
+
template = PromptTemplate(
|
|
123
|
+
input_variables=prompt_data.get("input_variables", []),
|
|
124
|
+
template=prompt_data["template"],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
self._prompts_cache[prompt_name] = template
|
|
128
|
+
return template
|
|
129
|
+
|
|
130
|
+
def render_prompt(self, name: str, context: dict) -> str:
|
|
131
|
+
"""プロンプトテンプレートから必要な引数を自動で取得"""
|
|
132
|
+
prompt_template = self.load_prompt(name)
|
|
133
|
+
|
|
134
|
+
# 必要なキーだけ抽出して format に渡す
|
|
135
|
+
subset = {
|
|
136
|
+
k: context[k] for k in prompt_template.input_variables if k in context
|
|
137
|
+
}
|
|
138
|
+
print(f"debug subset = {subset}")
|
|
139
|
+
return prompt_template.template.format(**subset)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ViewPointResult(BaseModel):
|
|
143
|
+
"""判断軸のstructured output用Pydanticモデル"""
|
|
144
|
+
|
|
145
|
+
viewpoint: str = Field(description="官庁への申請が必要な場合の観点")
|
|
146
|
+
annotation: str = Field(description="注釈")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class BaseExtractor(ABC):
|
|
150
|
+
"""抽出処理の基底クラス"""
|
|
151
|
+
|
|
152
|
+
def __init__(self, llm: BaseLLM, prompt_manager: PromptManager, prompt_name: str):
|
|
153
|
+
self.llm = llm
|
|
154
|
+
self.prompt_manager = prompt_manager
|
|
155
|
+
self.prompt_name = prompt_name
|
|
156
|
+
|
|
157
|
+
@abstractmethod
|
|
158
|
+
def extract(self, state: GraphState) -> ExtractionResult:
|
|
159
|
+
"""抽出処理の実行.ここで_create_messagesと_invoke_llmを呼び出す"""
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
def _create_messages(self, formatted_prompt: str) -> List[BaseMessage]:
|
|
163
|
+
"""メッセージリストの生成"""
|
|
164
|
+
return [SystemMessage(content=formatted_prompt)]
|
|
165
|
+
|
|
166
|
+
def _invoke_llm(self, messages: List[BaseMessage]) -> str:
|
|
167
|
+
"""LLMの呼び出しと例外処理"""
|
|
168
|
+
try:
|
|
169
|
+
response = self.llm.invoke(messages)
|
|
170
|
+
return response.content if hasattr(response, "content") else str(response)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"LLM呼び出しエラー: {e}")
|
|
173
|
+
raise
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class LawExtractor(BaseExtractor):
|
|
177
|
+
"""法令本体からの関連条文抽出"""
|
|
178
|
+
|
|
179
|
+
def extract(self, state: GraphState) -> ExtractionResult:
|
|
180
|
+
"""法令から関連条文を抽出"""
|
|
181
|
+
logger.info("法令からの関連条文抽出を開始")
|
|
182
|
+
|
|
183
|
+
# FIXME :: プロンプトのより良い読み込み方法を考える.
|
|
184
|
+
prompt_template = self.prompt_manager.load_prompt(self.prompt_name)
|
|
185
|
+
formatted_prompt = prompt_template.format(
|
|
186
|
+
law_name=state["law_document"].name,
|
|
187
|
+
law_article=", ".join(state["target_articles"]),
|
|
188
|
+
law_text=state["law_document"].content,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
messages = self._create_messages(formatted_prompt)
|
|
192
|
+
extracted_content = self._invoke_llm(messages)
|
|
193
|
+
|
|
194
|
+
return ExtractionResult(
|
|
195
|
+
content=extracted_content,
|
|
196
|
+
metadata={
|
|
197
|
+
"stage": "law_extraction",
|
|
198
|
+
"source_document": state["law_document"].name,
|
|
199
|
+
"target_articles": state["target_articles"],
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class RegulationExtractor(BaseExtractor):
|
|
205
|
+
"""施行規則からの関連条文抽出"""
|
|
206
|
+
|
|
207
|
+
def extract(self, state: GraphState) -> ExtractionResult:
|
|
208
|
+
"""施行規則から関連条文を抽出"""
|
|
209
|
+
logger.info("施行規則からの関連条文抽出を開始")
|
|
210
|
+
|
|
211
|
+
# FIXME :: プロンプトのより良い読み込み方法を考える.
|
|
212
|
+
prompt_template = self.prompt_manager.load_prompt(self.prompt_name)
|
|
213
|
+
formatted_prompt = prompt_template.format(
|
|
214
|
+
law_name=state["law_document"].name,
|
|
215
|
+
extracted_law_content=state["extracted_law_content"],
|
|
216
|
+
regulation_text=state["regulation_document"].content,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
messages = self._create_messages(formatted_prompt)
|
|
220
|
+
extracted_content = self._invoke_llm(messages)
|
|
221
|
+
|
|
222
|
+
return ExtractionResult(
|
|
223
|
+
content=extracted_content,
|
|
224
|
+
metadata={
|
|
225
|
+
"stage": "regulation_extraction",
|
|
226
|
+
"source_document": state["regulation_document"].name,
|
|
227
|
+
"law_reference": state["law_document"].name,
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class ViewpointGenerator(BaseExtractor):
|
|
233
|
+
"""最終判断軸生成"""
|
|
234
|
+
|
|
235
|
+
def extract(self, state: GraphState) -> ExtractionResult:
|
|
236
|
+
"""抽出した法令(state[extracted_law_content],state[extracted_regulation_content])を利用して,最終的な要点を生成"""
|
|
237
|
+
logger.info("最終的な判断軸生成を開始")
|
|
238
|
+
|
|
239
|
+
# プロンプトの読み込み
|
|
240
|
+
base_context = flatten_state(state)
|
|
241
|
+
special_context = {
|
|
242
|
+
"law_name": state["law_document"].name,
|
|
243
|
+
"law_article": ", ".join(state["target_articles"]),
|
|
244
|
+
"law_text": state["extracted_law_content"],
|
|
245
|
+
"enforcement_regulations": state["extracted_regulation_content"],
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
formatted_prompt = self.prompt_manager.render_prompt(
|
|
249
|
+
self.prompt_name,
|
|
250
|
+
context={**base_context, **special_context},
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
messages = self._create_messages(formatted_prompt)
|
|
254
|
+
structured_llm = self.llm.with_structured_output(ViewPointResult)
|
|
255
|
+
summary = structured_llm.invoke(messages)
|
|
256
|
+
|
|
257
|
+
return ExtractionResult(
|
|
258
|
+
content=summary,
|
|
259
|
+
metadata={
|
|
260
|
+
"stage": "summary_generation",
|
|
261
|
+
"law_document": state["law_document"].name,
|
|
262
|
+
"regulation_document": state["regulation_document"].name,
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class GraphBuilder:
|
|
268
|
+
"""法令要点抽出のグラフビルダー"""
|
|
269
|
+
|
|
270
|
+
# 各LLM呼び出しのプロンプト名
|
|
271
|
+
DEFAULT_PROMPT_NAMES = {
|
|
272
|
+
"extract_law": "extract_laws_v001",
|
|
273
|
+
"extract_regulation": "extract_regulation_v001",
|
|
274
|
+
"generate_summary": "v003",
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
def __init__(
|
|
278
|
+
self,
|
|
279
|
+
llm: BaseLLM,
|
|
280
|
+
prompts_dir: Path = Path("prompts"),
|
|
281
|
+
prompt_names: Optional[Dict[str, str]] = None,
|
|
282
|
+
):
|
|
283
|
+
self.llm = llm
|
|
284
|
+
self.prompt_manager = PromptManager(prompts_dir)
|
|
285
|
+
|
|
286
|
+
# デフォルトとユーザ指定をマージ(ユーザ指定が優先)
|
|
287
|
+
self.prompt_names = {**self.DEFAULT_PROMPT_NAMES, **(prompt_names or {})}
|
|
288
|
+
|
|
289
|
+
# 各抽出器の初期化
|
|
290
|
+
self.law_extractor = LawExtractor(
|
|
291
|
+
llm, self.prompt_manager, self.prompt_names["extract_law"]
|
|
292
|
+
)
|
|
293
|
+
self.regulation_extractor = RegulationExtractor(
|
|
294
|
+
llm, self.prompt_manager, self.prompt_names["extract_regulation"]
|
|
295
|
+
)
|
|
296
|
+
self.summary_generator = ViewpointGenerator(
|
|
297
|
+
llm, self.prompt_manager, self.prompt_names["generate_summary"]
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# グラフの構築
|
|
301
|
+
self.graph = self._build_graph()
|
|
302
|
+
|
|
303
|
+
def _build_graph(self): # TODO:: CompiledGraph型の戻り値を指定
|
|
304
|
+
"""LangGraphの構築"""
|
|
305
|
+
workflow = StateGraph(GraphState)
|
|
306
|
+
|
|
307
|
+
# ノードの追加
|
|
308
|
+
workflow.add_node("extract_law", self._extract_law_node)
|
|
309
|
+
workflow.add_node("extract_regulation", self._extract_regulation_node)
|
|
310
|
+
workflow.add_node("generate_summary", self._generate_summary_node)
|
|
311
|
+
workflow.add_node("handle_error", self._handle_error_node)
|
|
312
|
+
|
|
313
|
+
# エッジの設定
|
|
314
|
+
workflow.set_entry_point("extract_law")
|
|
315
|
+
|
|
316
|
+
workflow.add_conditional_edges(
|
|
317
|
+
"extract_law",
|
|
318
|
+
self._should_continue_to_regulation,
|
|
319
|
+
{"continue": "extract_regulation", "error": "handle_error"},
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
workflow.add_conditional_edges(
|
|
323
|
+
"extract_regulation",
|
|
324
|
+
self._should_continue_to_summary,
|
|
325
|
+
{"continue": "generate_summary", "error": "handle_error"},
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
workflow.add_edge("generate_summary", END)
|
|
329
|
+
workflow.add_edge("handle_error", END)
|
|
330
|
+
|
|
331
|
+
return workflow.compile()
|
|
332
|
+
|
|
333
|
+
def _extract_law_node(self, state: GraphState) -> GraphState:
|
|
334
|
+
"""法令抽出ノード"""
|
|
335
|
+
try:
|
|
336
|
+
result = self.law_extractor.extract(state)
|
|
337
|
+
state["extracted_law_content"] = result.content
|
|
338
|
+
state["current_stage"] = ProcessingStage.LAW_EXTRACTION
|
|
339
|
+
state["metadata"].update(result.metadata)
|
|
340
|
+
logger.info("法令抽出が完了しました")
|
|
341
|
+
except Exception as e:
|
|
342
|
+
state["error_message"] = f"法令抽出エラー: {str(e)}"
|
|
343
|
+
logger.error(state["error_message"])
|
|
344
|
+
|
|
345
|
+
return state
|
|
346
|
+
|
|
347
|
+
def _extract_regulation_node(self, state: GraphState) -> GraphState:
|
|
348
|
+
"""施行規則抽出ノード"""
|
|
349
|
+
try:
|
|
350
|
+
result = self.regulation_extractor.extract(state)
|
|
351
|
+
state["extracted_regulation_content"] = result.content
|
|
352
|
+
state["current_stage"] = ProcessingStage.REGULATION_EXTRACTION
|
|
353
|
+
state["metadata"].update(result.metadata)
|
|
354
|
+
logger.info("施行規則抽出が完了しました")
|
|
355
|
+
except Exception as e:
|
|
356
|
+
state["error_message"] = f"施行規則抽出エラー: {str(e)}"
|
|
357
|
+
logger.error(state["error_message"])
|
|
358
|
+
|
|
359
|
+
return state
|
|
360
|
+
|
|
361
|
+
def _generate_summary_node(self, state: GraphState) -> GraphState:
|
|
362
|
+
"""要点生成ノード"""
|
|
363
|
+
try:
|
|
364
|
+
result = self.summary_generator.extract(state)
|
|
365
|
+
state["final_summary"] = result.content
|
|
366
|
+
state["current_stage"] = ProcessingStage.COMPLETED
|
|
367
|
+
state["metadata"].update(result.metadata)
|
|
368
|
+
logger.info("要点生成が完了しました")
|
|
369
|
+
except Exception as e:
|
|
370
|
+
state["error_message"] = f"要点生成エラー: {str(e)}"
|
|
371
|
+
logger.error(state["error_message"])
|
|
372
|
+
|
|
373
|
+
return state
|
|
374
|
+
|
|
375
|
+
def _handle_error_node(self, state: GraphState) -> GraphState:
|
|
376
|
+
"""エラーハンドリングノード"""
|
|
377
|
+
logger.error(
|
|
378
|
+
f"処理中にエラーが発生しました: {state.get('error_message', '不明なエラー')}"
|
|
379
|
+
)
|
|
380
|
+
state["current_stage"] = ProcessingStage.COMPLETED
|
|
381
|
+
return state
|
|
382
|
+
|
|
383
|
+
def _should_continue_to_regulation(self, state: GraphState) -> str:
|
|
384
|
+
"""施行規則抽出への継続判定"""
|
|
385
|
+
return "error" if state.get("error_message") else "continue"
|
|
386
|
+
|
|
387
|
+
def _should_continue_to_summary(self, state: GraphState) -> str:
|
|
388
|
+
"""要点生成への継続判定"""
|
|
389
|
+
return "error" if state.get("error_message") else "continue"
|
|
390
|
+
|
|
391
|
+
def process(
|
|
392
|
+
self,
|
|
393
|
+
law_document: LegalDocument,
|
|
394
|
+
regulation_document: LegalDocument,
|
|
395
|
+
target_articles: List[str],
|
|
396
|
+
) -> GraphState:
|
|
397
|
+
"""処理の実行"""
|
|
398
|
+
initial_state = GraphState(
|
|
399
|
+
law_document=law_document,
|
|
400
|
+
regulation_document=regulation_document,
|
|
401
|
+
target_articles=target_articles,
|
|
402
|
+
extracted_law_content=None,
|
|
403
|
+
extracted_regulation_content=None,
|
|
404
|
+
final_summary=None,
|
|
405
|
+
current_stage=ProcessingStage.LAW_EXTRACTION,
|
|
406
|
+
error_message=None,
|
|
407
|
+
metadata={},
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
logger.info("法令判断軸抽出処理を開始します")
|
|
411
|
+
result = self.graph.invoke(initial_state)
|
|
412
|
+
logger.info(f"処理が完了しました。ステージ: {result['current_stage']}")
|
|
413
|
+
|
|
414
|
+
return result
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class LegalExtractionConfig:
|
|
418
|
+
"""設定管理クラス"""
|
|
419
|
+
|
|
420
|
+
def __init__(
|
|
421
|
+
self,
|
|
422
|
+
llm,
|
|
423
|
+
prompts_dir: str = "prompts",
|
|
424
|
+
prompt_names: Optional[Dict[str, str]] = None,
|
|
425
|
+
):
|
|
426
|
+
self.llm = llm
|
|
427
|
+
self.prompts_dir = Path(prompts_dir)
|
|
428
|
+
self.prompt_names = prompt_names
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def create_legal_extraction_system(config: LegalExtractionConfig) -> GraphBuilder:
|
|
432
|
+
"""法令要点抽出システムのファクトリー関数"""
|
|
433
|
+
return GraphBuilder(
|
|
434
|
+
llm=config.llm, prompts_dir=config.prompts_dir, prompt_names=config.prompt_names
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# 使用例
|
|
439
|
+
def example_usage():
|
|
440
|
+
"""使用例の実装"""
|
|
441
|
+
|
|
442
|
+
# 設定の初期化
|
|
443
|
+
config = LegalExtractionConfig(
|
|
444
|
+
model_name="gpt-4o-mini",
|
|
445
|
+
temperature=0.1,
|
|
446
|
+
prompts_dir="prompts",
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# システムの初期化
|
|
450
|
+
extraction_system = create_legal_extraction_system(config)
|
|
451
|
+
|
|
452
|
+
# サンプルデータ
|
|
453
|
+
law_document = LegalDocument(
|
|
454
|
+
name="土壌汚染対策法", content="(ここに法令本文が入る)", document_type="law"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
regulation_document = LegalDocument(
|
|
458
|
+
name="土壌汚染対策法施行規則",
|
|
459
|
+
content="(ここに施行規則本文が入る)",
|
|
460
|
+
document_type="regulation",
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
target_articles = ["第3条", "第4条"]
|
|
464
|
+
|
|
465
|
+
# 処理の実行
|
|
466
|
+
try:
|
|
467
|
+
result = extraction_system.process(
|
|
468
|
+
law_document=law_document,
|
|
469
|
+
regulation_document=regulation_document,
|
|
470
|
+
target_articles=target_articles,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
if result["error_message"]:
|
|
474
|
+
print(f"エラーが発生しました: {result['error_message']}")
|
|
475
|
+
else:
|
|
476
|
+
print("=== 最終要点 ===")
|
|
477
|
+
print(result["final_summary"])
|
|
478
|
+
|
|
479
|
+
print("\n=== メタデータ ===")
|
|
480
|
+
for key, value in result["metadata"].items():
|
|
481
|
+
print(f"{key}: {value}")
|
|
482
|
+
|
|
483
|
+
except Exception as e:
|
|
484
|
+
logger.error(f"処理中に予期しないエラーが発生しました: {e}")
|