llm-graph-kit 0.1.0__tar.gz
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.
- llm_graph_kit-0.1.0/PKG-INFO +11 -0
- llm_graph_kit-0.1.0/README.md +2 -0
- llm_graph_kit-0.1.0/pyproject.toml +22 -0
- llm_graph_kit-0.1.0/src/llm_graph_kit/.gitkeep +0 -0
- llm_graph_kit-0.1.0/src/llm_graph_kit/__init__.py +9 -0
- llm_graph_kit-0.1.0/src/llm_graph_kit/graph_logger.py +118 -0
- llm_graph_kit-0.1.0/src/llm_graph_kit/llm_graph_kit.py +196 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: llm-graph-kit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Dist: mlx-augllm>=1.5
|
|
6
|
+
Requires-Python: >=3.13.9
|
|
7
|
+
Project-URL: Homepage, https://github.com/ToPo-ToPo-ToPo/llm_graph
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# llm_graph
|
|
11
|
+
LLMベースのエージェント作成ライブラリ
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "llm-graph-kit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13.9"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"mlx-augllm>=1.5",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[project.urls]
|
|
12
|
+
Homepage = "https://github.com/ToPo-ToPo-ToPo/llm_graph"
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["uv_build >= 0.9.21"]
|
|
16
|
+
build-backend = "uv_build"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = [
|
|
20
|
+
"build>=1.4.0",
|
|
21
|
+
"twine>=6.2.0",
|
|
22
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import textwrap
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
class GraphLogger:
|
|
5
|
+
"""
|
|
6
|
+
プログラムの内容に依存せず、ターミナル表示の「スタイル」を提供する汎用ロガー。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# 色設定
|
|
10
|
+
COLORS = {
|
|
11
|
+
"HEADER": '\033[95m', "BLUE": '\033[94m', "CYAN": '\033[96m',
|
|
12
|
+
"GREEN": '\033[92m', "YELLOW": '\033[93m', "RED": '\033[91m',
|
|
13
|
+
"ENDC": '\033[0m', "BOLD": '\033[1m'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def print_phase_header(cls, title: str, emoji: str = "🚀"):
|
|
18
|
+
"""メインフェーズの開始を目立つように表示します"""
|
|
19
|
+
c = cls.COLORS
|
|
20
|
+
print(f"\n{c['BLUE']}{c['BOLD']}" + "="*70 + f"{c['ENDC']}")
|
|
21
|
+
print(f"{c['BLUE']}{c['BOLD']} {emoji} {title} {c['ENDC']}")
|
|
22
|
+
print(f"{c['BLUE']}{c['BOLD']}" + "="*70 + f"{c['ENDC']}")
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def print_subtask_start(cls, index: int, task_name: str):
|
|
26
|
+
"""サブタスクの開始を表示します"""
|
|
27
|
+
c = cls.COLORS
|
|
28
|
+
print(f"\n{c['YELLOW']}┌── 🔸 Subtask {index} ──────────────────────────────────────────────────{c['ENDC']}")
|
|
29
|
+
print(f"{c['YELLOW']}│ Task: {c['ENDC']}{task_name}")
|
|
30
|
+
print(f"{c['YELLOW']}└──────────────────────────────────────────────────────────────────{c['ENDC']}")
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def log(cls, style: str, content: Any, title: str = ""):
|
|
34
|
+
"""
|
|
35
|
+
スタイルを指定してログを出力します。
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
style (str): 表示スタイル ("header", "box", "list", "info", "success", "error", "code")
|
|
39
|
+
content (Any): 表示内容(文字列、リスト、辞書など)
|
|
40
|
+
title (str): タイトルやラベル(任意)
|
|
41
|
+
"""
|
|
42
|
+
c = cls.COLORS
|
|
43
|
+
style = style.lower()
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------
|
|
46
|
+
# 1.
|
|
47
|
+
# ---------------------------------------------------------
|
|
48
|
+
if style == "response":
|
|
49
|
+
# タイトルが指定されていなければデフォルトを設定
|
|
50
|
+
display_title = title if title else "Generated Response"
|
|
51
|
+
|
|
52
|
+
# 色設定 (ここでは CYAN を使用。GREEN にしたい場合は c['GREEN'] に変更可)
|
|
53
|
+
color = c['GREEN']
|
|
54
|
+
|
|
55
|
+
print(f"\n{color}{c['BOLD']}🤖 {display_title}{c['ENDC']}")
|
|
56
|
+
print(f"{color}──────────────────────────────────────────────────────────────{c['ENDC']}")
|
|
57
|
+
|
|
58
|
+
# 本文も色付きで表示
|
|
59
|
+
print(f"{color}{content}{c['ENDC']}")
|
|
60
|
+
|
|
61
|
+
print(f"{color}──────────────────────────────────────────────────────────────{c['ENDC']}\n")
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------
|
|
64
|
+
# 3. list: 計画や手順の箇条書き
|
|
65
|
+
# ---------------------------------------------------------
|
|
66
|
+
elif style == "list":
|
|
67
|
+
if title:
|
|
68
|
+
print(f"\n{c['BOLD']}📋 {title}:{c['ENDC']}")
|
|
69
|
+
|
|
70
|
+
if isinstance(content, list):
|
|
71
|
+
for i, item in enumerate(content, 1):
|
|
72
|
+
print(f"{i}. {item}")
|
|
73
|
+
else:
|
|
74
|
+
print(f"- {content}")
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------
|
|
77
|
+
# 4. info: 一般的な情報、ツール選択など(1行表示推奨)
|
|
78
|
+
# ---------------------------------------------------------
|
|
79
|
+
elif style == "info":
|
|
80
|
+
# 辞書が渡された場合は Key: Value 形式で見やすく
|
|
81
|
+
if isinstance(content, dict):
|
|
82
|
+
print(f"{c['CYAN']}🛠 {title}{c['ENDC']}")
|
|
83
|
+
for k, v in content.items():
|
|
84
|
+
print(f"Running {k}: {v}")
|
|
85
|
+
else:
|
|
86
|
+
label = f"{title}: " if title else ""
|
|
87
|
+
print(f"{c['CYAN']}ℹ️ {label}{c['BOLD']}{content}{c['ENDC']}")
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------
|
|
90
|
+
# 5. code / preview: 実行結果などの長文プレビュー
|
|
91
|
+
# ---------------------------------------------------------
|
|
92
|
+
elif style == "code" or style == "preview":
|
|
93
|
+
text = str(content)
|
|
94
|
+
# 長すぎる場合は省略表示
|
|
95
|
+
preview = textwrap.shorten(text, width=200, placeholder="...")
|
|
96
|
+
lines = preview.split('\n')
|
|
97
|
+
if len(lines) > 5:
|
|
98
|
+
preview = "\n".join(lines[:5]) + "\n... (more lines) ..."
|
|
99
|
+
|
|
100
|
+
label = title if title else "Output"
|
|
101
|
+
print(f"{c['GREEN']}📄 {label}:\n{preview}{c['ENDC']}")
|
|
102
|
+
print(f"{c['GREEN']}──────────────────────────────────────{c['ENDC']}")
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------
|
|
105
|
+
# 6. success / error: 評価や完了通知
|
|
106
|
+
# ---------------------------------------------------------
|
|
107
|
+
elif style == "success":
|
|
108
|
+
print(f"{c['GREEN']}✅ {title}: {content}{c['ENDC']}")
|
|
109
|
+
|
|
110
|
+
elif style == "error":
|
|
111
|
+
print(f"{c['RED']}❌ {title}: {content}{c['ENDC']}")
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------
|
|
114
|
+
# 7. fallback: 想定外のスタイル
|
|
115
|
+
# ---------------------------------------------------------
|
|
116
|
+
else:
|
|
117
|
+
prefix = f"[{title}] " if title else ""
|
|
118
|
+
print(f"{prefix}{content}")
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from typing import Dict, Any, Callable, Union, Tuple
|
|
2
|
+
|
|
3
|
+
# ステートの型定義
|
|
4
|
+
NodeState = Dict[str, Any]
|
|
5
|
+
|
|
6
|
+
class LLMGraph:
|
|
7
|
+
"""
|
|
8
|
+
ノードとエッジで構成されるステートマシンエンジン。
|
|
9
|
+
Router関数だけでなく、ステートの値を直接参照するルーティングに対応しました。
|
|
10
|
+
"""
|
|
11
|
+
# 定数定義
|
|
12
|
+
START = "__START__"
|
|
13
|
+
END = "__END__"
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
# ノード名 -> 関数
|
|
17
|
+
self.nodes: Dict[str, Callable[[NodeState], NodeState]] = {}
|
|
18
|
+
# ノード名 -> 次のノード名 または (条件, マッピング辞書)
|
|
19
|
+
# 条件は Callable(関数) または str(ステートのキー)
|
|
20
|
+
self.edges: Dict[str, Union[str, Tuple[Union[Callable, str], Dict[str, str]]]] = {}
|
|
21
|
+
self.entry_point: str = ""
|
|
22
|
+
self.subgraphs: Dict[str, 'LLMGraph'] = {}
|
|
23
|
+
|
|
24
|
+
def add_node(self, name: str, func: Callable[[NodeState], NodeState]):
|
|
25
|
+
"""ノードを登録します"""
|
|
26
|
+
self.nodes[name] = func
|
|
27
|
+
|
|
28
|
+
def add_node_with_subgraph(self, name: str, func: Callable[[NodeState], NodeState], subgraph: 'LLMGraph'):
|
|
29
|
+
"""サブグラフ構造を持つノードを登録します"""
|
|
30
|
+
self.nodes[name] = func
|
|
31
|
+
self.subgraphs[name] = subgraph
|
|
32
|
+
|
|
33
|
+
def add_edge(self, from_node: str, to_node: str):
|
|
34
|
+
"""固定ルートを定義します"""
|
|
35
|
+
if from_node == self.START:
|
|
36
|
+
self.entry_point = to_node
|
|
37
|
+
return
|
|
38
|
+
self.edges[from_node] = to_node
|
|
39
|
+
|
|
40
|
+
def add_conditional_edge(
|
|
41
|
+
self,
|
|
42
|
+
from_node: str,
|
|
43
|
+
condition: Union[Callable[[NodeState], str], str],
|
|
44
|
+
path_map: Dict[str, str]
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
条件分岐ルートを定義します。
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
from_node: 分岐元のノード名
|
|
51
|
+
condition:
|
|
52
|
+
- 関数: NodeStateを受け取りシグナル(文字列)を返す
|
|
53
|
+
- 文字列: シグナルが格納されているNodeStateのキー名
|
|
54
|
+
path_map: { "シグナル": "行き先のノード名" } の辞書
|
|
55
|
+
"""
|
|
56
|
+
self.edges[from_node] = (condition, path_map)
|
|
57
|
+
|
|
58
|
+
def add_subgraph(self, node_name: str, subgraph: 'LLMGraph'):
|
|
59
|
+
"""可視化用にサブグラフを登録します"""
|
|
60
|
+
self.subgraphs[node_name] = subgraph
|
|
61
|
+
|
|
62
|
+
def run(self, initial_state: NodeState) -> NodeState:
|
|
63
|
+
"""グラフを実行します"""
|
|
64
|
+
if not self.entry_point:
|
|
65
|
+
raise ValueError("Entry point not set. Use add_edge(Graph.START, 'node_name').")
|
|
66
|
+
|
|
67
|
+
current_node_name = self.entry_point
|
|
68
|
+
state = initial_state.copy()
|
|
69
|
+
|
|
70
|
+
while current_node_name != self.END:
|
|
71
|
+
if current_node_name not in self.nodes:
|
|
72
|
+
raise ValueError(f"Node '{current_node_name}' is not defined!")
|
|
73
|
+
|
|
74
|
+
# ノード実行
|
|
75
|
+
node_func = self.nodes[current_node_name]
|
|
76
|
+
new_data = node_func(state)
|
|
77
|
+
if new_data:
|
|
78
|
+
state.update(new_data)
|
|
79
|
+
|
|
80
|
+
# --- 次の行き先を決定 ---
|
|
81
|
+
edge_data = self.edges.get(current_node_name)
|
|
82
|
+
|
|
83
|
+
if edge_data is None:
|
|
84
|
+
current_node_name = self.END
|
|
85
|
+
|
|
86
|
+
# 条件付きエッジ (Func/Key, Map)
|
|
87
|
+
elif isinstance(edge_data, tuple):
|
|
88
|
+
condition, path_map = edge_data
|
|
89
|
+
|
|
90
|
+
# ★改良点: 関数なら実行、文字列ならステートから取得
|
|
91
|
+
if callable(condition):
|
|
92
|
+
signal = condition(state)
|
|
93
|
+
elif isinstance(condition, str):
|
|
94
|
+
signal = state.get(condition)
|
|
95
|
+
if signal is None:
|
|
96
|
+
raise ValueError(f"NodeState key '{condition}' not found for routing from '{current_node_name}'")
|
|
97
|
+
else:
|
|
98
|
+
raise ValueError("Invalid condition type in edge")
|
|
99
|
+
|
|
100
|
+
# 文字列化(Enum対応)
|
|
101
|
+
signal_str = str(signal).split('.')[-1] if hasattr(signal, 'name') else str(signal)
|
|
102
|
+
|
|
103
|
+
# マッピング解決
|
|
104
|
+
next_dest = None
|
|
105
|
+
for key, val in path_map.items():
|
|
106
|
+
key_str = str(key).split('.')[-1] if hasattr(key, 'name') else str(key)
|
|
107
|
+
if key_str == signal_str:
|
|
108
|
+
next_dest = val
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
if next_dest:
|
|
112
|
+
print(f"[Router decision]: '{signal_str}' => Go to [{next_dest}]")
|
|
113
|
+
current_node_name = next_dest
|
|
114
|
+
else:
|
|
115
|
+
raise ValueError(f"Router returned '{signal}', but not found in map: {path_map}")
|
|
116
|
+
|
|
117
|
+
# 固定エッジ
|
|
118
|
+
else:
|
|
119
|
+
current_node_name = edge_data
|
|
120
|
+
|
|
121
|
+
return state
|
|
122
|
+
|
|
123
|
+
# ==========================================================================
|
|
124
|
+
# 可視化 (Mermaid) [改良版]
|
|
125
|
+
# ==========================================================================
|
|
126
|
+
def get_graph_mermaid(self) -> str:
|
|
127
|
+
lines = ["graph TD"]
|
|
128
|
+
|
|
129
|
+
# スタイル定義
|
|
130
|
+
lines.append(" %% Styles")
|
|
131
|
+
lines.append(" classDef startClass fill:#f9f,stroke:#333,stroke-width:2px,rx:10,ry:10;")
|
|
132
|
+
lines.append(" classDef endClass fill:#f96,stroke:#333,stroke-width:2px,rx:10,ry:10;")
|
|
133
|
+
lines.append(" classDef nodeClass fill:#e1f5fe,stroke:#0277bd,stroke-width:2px,rx:5,ry:5;")
|
|
134
|
+
lines.append(" classDef routerClass fill:#fff9c4,stroke:#fbc02d,stroke-width:2px,stroke-dasharray: 5 5,rhombus;")
|
|
135
|
+
lines.append(" classDef subStartClass fill:#eee,stroke:#999,stroke-width:1px,rx:5,ry:5;")
|
|
136
|
+
|
|
137
|
+
def render_content(graph_obj, prefix="", is_subgraph=False):
|
|
138
|
+
# START / END
|
|
139
|
+
style = "subStartClass" if is_subgraph else "startClass"
|
|
140
|
+
end_style = "subStartClass" if is_subgraph else "endClass"
|
|
141
|
+
lines.append(f" {prefix}{self.START}(START):::{style}")
|
|
142
|
+
lines.append(f" {prefix}{self.END}(END):::{end_style}")
|
|
143
|
+
|
|
144
|
+
# ノード
|
|
145
|
+
for node in graph_obj.nodes:
|
|
146
|
+
node_id = f"{prefix}{node}"
|
|
147
|
+
lines.append(f" {node_id}[{node}]:::nodeClass")
|
|
148
|
+
|
|
149
|
+
# Entry Point
|
|
150
|
+
if graph_obj.entry_point:
|
|
151
|
+
lines.append(f" {prefix}{self.START} --> {prefix}{graph_obj.entry_point}")
|
|
152
|
+
|
|
153
|
+
# Edges
|
|
154
|
+
for from_node, edge_data in graph_obj.edges.items():
|
|
155
|
+
from_id = f"{prefix}{from_node}"
|
|
156
|
+
|
|
157
|
+
if isinstance(edge_data, tuple):
|
|
158
|
+
condition, path_map = edge_data
|
|
159
|
+
router_id = f"{prefix}router_{from_node}"
|
|
160
|
+
|
|
161
|
+
# ラベルの決定(関数名 or キー名)
|
|
162
|
+
if callable(condition):
|
|
163
|
+
label = f"{{{condition.__name__}}}"
|
|
164
|
+
else:
|
|
165
|
+
label = f"{{{condition}}}" # ステートのキー名を表示
|
|
166
|
+
|
|
167
|
+
lines.append(f" {from_id} --> {router_id}{label}:::routerClass")
|
|
168
|
+
|
|
169
|
+
for signal, to_node in path_map.items():
|
|
170
|
+
to_id = f"{prefix}{to_node}"
|
|
171
|
+
s_label = str(signal).split('.')[-1]
|
|
172
|
+
lines.append(f" {router_id} -- {s_label} --> {to_id}")
|
|
173
|
+
else:
|
|
174
|
+
to_id = f"{prefix}{edge_data}"
|
|
175
|
+
lines.append(f" {from_id} --> {to_id}")
|
|
176
|
+
|
|
177
|
+
if self.subgraphs:
|
|
178
|
+
# サブグラフ描画
|
|
179
|
+
for node_name, sub_graph in self.subgraphs.items():
|
|
180
|
+
cluster_id = f"Sub_{node_name}"
|
|
181
|
+
lines.append(f" subgraph {cluster_id} [\"{node_name}\"]")
|
|
182
|
+
lines.append(" direction TD")
|
|
183
|
+
lines.append(f" style {cluster_id} fill:#fffde7,stroke:#fbc02d,stroke-width:2px")
|
|
184
|
+
render_content(sub_graph, prefix=f"{node_name}_", is_subgraph=True)
|
|
185
|
+
lines.append(" end")
|
|
186
|
+
|
|
187
|
+
# メイングラフ描画
|
|
188
|
+
lines.append(" subgraph Main [Main Workflow]")
|
|
189
|
+
lines.append(" style Main fill:none,stroke:none")
|
|
190
|
+
render_content(self, prefix="", is_subgraph=False)
|
|
191
|
+
lines.append(" end")
|
|
192
|
+
|
|
193
|
+
else:
|
|
194
|
+
render_content(self, prefix="", is_subgraph=False)
|
|
195
|
+
|
|
196
|
+
return "\n".join(lines)
|