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.
@@ -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,2 @@
1
+ # llm_graph
2
+ 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,9 @@
1
+
2
+ from .llm_graph_kit import LLMGraphKit, NodeState
3
+ from .graph_logger import GraphLogger
4
+
5
+ __all__ = [
6
+ "LLMGraphKit",
7
+ "NodeState",
8
+ "GraphLogger"
9
+ ]
@@ -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)