sp-llmgraph 0.4.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.
llmgraph/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """
2
+ LLMGraph — Formato binario de grafos para conocimiento de LLMs
3
+ """
4
+ from .schema import Graph, Node, Edge, UndirectedEdge, Property, VT
5
+ from .encoder import encode, decode_varint, encode_varint
6
+ from .decoder import decode
7
+ from .compressor import compress, decompress, is_compressed
8
+ from .retrieval import RetrievalEngine
9
+ from .context import (
10
+ context_build,
11
+ context_health,
12
+ context_init,
13
+ context_prepare,
14
+ context_search,
15
+ context_update,
16
+ )
17
+
18
+ __version__ = "0.4.0"
19
+ __all__ = ["Graph", "Node", "Edge", "UndirectedEdge", "Property", "VT",
20
+ "encode", "decode", "compress", "decompress", "is_compressed",
21
+ "RetrievalEngine",
22
+ "context_init", "context_build", "context_prepare",
23
+ "context_search", "context_health", "context_update",
24
+ "git_context", "get_git_context",
25
+ "registry", "register_project", "unregister_project",
26
+ "list_projects", "search_projects", "update_project"]
llmgraph/cli.py ADDED
@@ -0,0 +1,332 @@
1
+ """
2
+ LLMGraph CLI — Herramienta de línea de comandos.
3
+ """
4
+ from __future__ import annotations
5
+ import argparse
6
+ import json
7
+ import sys
8
+ import os
9
+ import time
10
+
11
+ from .schema import Graph
12
+ from .encoder import encode
13
+ from .decoder import decode
14
+ from .compressor import compress, decompress, is_compressed
15
+ from .md_parser import parse_file, parse_directory
16
+ from .context import (
17
+ context_build,
18
+ context_health,
19
+ context_init,
20
+ context_prepare,
21
+ context_search,
22
+ context_update,
23
+ )
24
+
25
+
26
+ def cmd_convert(args):
27
+ path = args.path
28
+ t0 = time.time()
29
+ if os.path.isdir(path):
30
+ g = parse_directory(path, recursive=not args.no_recursive)
31
+ else:
32
+ g = parse_file(path)
33
+
34
+ raw = encode(g)
35
+ raw_size = len(raw)
36
+
37
+ if args.compress:
38
+ raw = compress(raw, method=args.compress)
39
+ label = f" ({args.compress})"
40
+ else:
41
+ label = ""
42
+
43
+ output = args.output or (os.path.splitext(path)[0] + ".llgr")
44
+ with open(output, "wb") as f:
45
+ f.write(raw)
46
+
47
+ stats = g.stats()
48
+ elapsed = time.time() - t0
49
+ md_size = sum(os.path.getsize(os.path.join(dp, f))
50
+ for dp, _, fn in os.walk(path) for f in fn
51
+ if f.endswith(".md")) if os.path.isdir(path) else os.path.getsize(path)
52
+
53
+ print(f"Convertido: {path} → {output}{label}")
54
+ print(f" Nodos: {stats['nodes']} ({', '.join(f'{k}:{v}' for k,v in stats['node_types'].items())})")
55
+ print(f" Edges: {stats['edges']}")
56
+ print(f" .md original: {md_size:,} bytes")
57
+ print(f" .llgr binario: {raw_size:,} bytes")
58
+ if args.compress:
59
+ print(f" .llgr comprimido: {len(raw):,} bytes")
60
+ print(f" Compresión vs .md: {md_size/max(len(raw),1):.1f}x")
61
+ print(f" Tiempo: {elapsed:.2f}s")
62
+
63
+
64
+ def cmd_encode(args):
65
+ if args.input:
66
+ g = parse_file(args.input)
67
+ else:
68
+ g = Graph()
69
+ raw = encode(g)
70
+ if args.output:
71
+ with open(args.output, "wb") as f:
72
+ f.write(raw)
73
+ print(f"Guardado: {args.output} ({len(raw)} bytes)")
74
+ else:
75
+ sys.stdout.buffer.write(raw)
76
+
77
+
78
+ def cmd_decode(args):
79
+ if args.input:
80
+ with open(args.input, "rb") as f:
81
+ raw = f.read()
82
+ else:
83
+ raw = sys.stdin.buffer.read()
84
+ if is_compressed(raw):
85
+ raw = decompress(raw)
86
+ g = decode(raw)
87
+ stats = g.stats()
88
+ print(json.dumps(stats, indent=2))
89
+
90
+
91
+ def cmd_query(args):
92
+ with open(args.path, "rb") as f:
93
+ raw = f.read()
94
+ if is_compressed(raw):
95
+ raw = decompress(raw)
96
+ g = decode(raw)
97
+
98
+ if args.type:
99
+ nodes = g.find(type=args.type)
100
+ for n in nodes:
101
+ print(f" [{n.id}] {n.type}: {n.get('name') or n.get('title') or '?'}")
102
+ elif args.edge_type:
103
+ edges = g.find_edges(type=args.edge_type)
104
+ for e in edges:
105
+ src_name = g.nodes[e.src].get('name') or g.nodes[e.src].get('title') or f'n{e.src}'
106
+ tgt_name = g.nodes[e.tgt].get('name') or g.nodes[e.tgt].get('title') or f'n{e.tgt}'
107
+ print(f" {src_name} --[{e.type}]--> {tgt_name}")
108
+ else:
109
+ print(json.dumps(g.stats(), indent=2))
110
+
111
+
112
+ def cmd_benchmark(args):
113
+ """Benchmark: compara .md vs .llgr vs .llgr comprimido."""
114
+ path = args.path
115
+ if os.path.isdir(path):
116
+ md_size = sum(os.path.getsize(os.path.join(dp, f))
117
+ for dp, _, fn in os.walk(path) for f in fn
118
+ if f.endswith(".md"))
119
+ g = parse_directory(path, recursive=not args.no_recursive)
120
+ else:
121
+ md_size = os.path.getsize(path)
122
+ g = parse_file(path)
123
+
124
+ raw = encode(g)
125
+ raw_z = compress(raw, method="zlib")
126
+
127
+ print(f"═══ Benchmark: {os.path.basename(path)} ═══")
128
+ print(f" .md original: {md_size:>10,} bytes (baseline)")
129
+ print(f" .llgr binario: {len(raw):>10,} bytes ({md_size/max(len(raw),1):.2f}x)")
130
+ print(f" .llgr + zlib: {len(raw_z):>10,} bytes ({md_size/max(len(raw_z),1):.2f}x)")
131
+ print(f" JSON completo: {len(json.dumps(g.to_dict()).encode()):>10,} bytes")
132
+ print(f" Nodes: {g.stats()['nodes']}, Edges: {g.stats()['edges']}")
133
+
134
+ # Timing
135
+ t0 = time.time()
136
+ for _ in range(10):
137
+ encode(g)
138
+ t_enc = (time.time() - t0) / 10
139
+
140
+ t0 = time.time()
141
+ for _ in range(10):
142
+ decode(raw)
143
+ t_dec = (time.time() - t0) / 10
144
+
145
+ print(f" Encode time: {t_enc*1000:.1f}ms")
146
+ print(f" Decode time: {t_dec*1000:.1f}ms")
147
+
148
+
149
+ def cmd_diff(args):
150
+ def _load(path):
151
+ with open(path, "rb") as f:
152
+ raw = f.read()
153
+ if is_compressed(raw):
154
+ raw = decompress(raw)
155
+ return decode(raw)
156
+
157
+ g1, g2 = _load(args.graph1), _load(args.graph2)
158
+ s1, s2 = g1.stats(), g2.stats()
159
+ print(f"Graph 1: {s1['nodes']} nodos, {s1['edges']} edges")
160
+ print(f"Graph 2: {s2['nodes']} nodos, {s2['edges']} edges")
161
+ all_types = set(list(s1['node_types'].keys()) + list(s2['node_types'].keys()))
162
+ for t in sorted(all_types):
163
+ c1 = s1['node_types'].get(t, 0)
164
+ c2 = s2['node_types'].get(t, 0)
165
+ if c1 != c2:
166
+ print(f" {t}: {c1} → {c2} ({'+' if c2 > c1 else ''}{c2-c1})")
167
+
168
+
169
+ def cmd_info(args):
170
+ """Mostrar info de un archivo .llgr."""
171
+ with open(args.path, "rb") as f:
172
+ raw = f.read()
173
+
174
+ if is_compressed(raw):
175
+ raw = decompress(raw)
176
+ g = decode(raw)
177
+ stats = g.stats()
178
+ # Detect method from original header
179
+ print(f"Archivo: {args.path}")
180
+ print(f" Tamaño disco: {os.path.getsize(args.path):,} bytes")
181
+ method = "desconocido"
182
+ with open(args.path, "rb") as f:
183
+ h = f.read(7)
184
+ if h[4:6] == b"ZL":
185
+ method = "zlib"
186
+ elif h[4:7] == b"LZ4":
187
+ method = "lz4"
188
+ print(f" Comprimido: {'sí (' + method + ')' if method != 'desconocido' else 'no'}")
189
+ print(f" Nodos: {stats['nodes']}")
190
+ print(f" Edges: {stats['edges']}")
191
+ print(f" Metadata: {stats['metadata_keys']} keys")
192
+ print(f" Tipos nodo: {stats['node_types']}")
193
+ print(f" Tipos edge: {stats['edge_types']}")
194
+
195
+
196
+ def cmd_context_init(args):
197
+ print(json.dumps(context_init(args.project), indent=2))
198
+
199
+
200
+ def cmd_context_build(args):
201
+ print(json.dumps(context_build(args.project), indent=2))
202
+
203
+
204
+ def cmd_context_prepare(args):
205
+ result = context_prepare(
206
+ args.project,
207
+ args.task,
208
+ profile_name=args.profile,
209
+ max_tokens=args.max_tokens,
210
+ save_pack=args.save,
211
+ )
212
+ if args.json:
213
+ print(json.dumps(result, indent=2))
214
+ else:
215
+ print(result["context_pack"])
216
+ if result.get("pack_path"):
217
+ print(f"\nSaved: {result['pack_path']}")
218
+
219
+
220
+ def cmd_context_search(args):
221
+ print(json.dumps(context_search(args.project, args.query, profile_name=args.profile, top_k=args.top_k), indent=2))
222
+
223
+
224
+ def cmd_context_health(args):
225
+ print(json.dumps(context_health(args.project), indent=2))
226
+
227
+
228
+ def cmd_context_update(args):
229
+ print(json.dumps(context_update(args.project, args.type, args.note, source=args.source), indent=2))
230
+
231
+
232
+ def main():
233
+ parser = argparse.ArgumentParser(
234
+ prog="llmgraph",
235
+ description="LLMGraph — Formato binario de grafos para LLMs v0.2"
236
+ )
237
+ sub = parser.add_subparsers(dest="command")
238
+
239
+ # convert
240
+ p = sub.add_parser("convert", help="Convertir .md a .llgr")
241
+ p.add_argument("path", help="Archivo o directorio .md")
242
+ p.add_argument("-o", "--output")
243
+ p.add_argument("--no-recursive", action="store_true")
244
+ p.add_argument("-c", "--compress", choices=["zlib", "lz4"], help="Comprimir post-encoding")
245
+ p.set_defaults(func=cmd_convert)
246
+
247
+ # encode
248
+ p = sub.add_parser("encode", help="Codificar grafo a binario")
249
+ p.add_argument("-i", "--input")
250
+ p.add_argument("-o", "--output")
251
+ p.set_defaults(func=cmd_encode)
252
+
253
+ # decode
254
+ p = sub.add_parser("decode", help="Decodificar grafo binario")
255
+ p.add_argument("-i", "--input")
256
+ p.set_defaults(func=cmd_decode)
257
+
258
+ # query
259
+ p = sub.add_parser("query", help="Consultar un grafo")
260
+ p.add_argument("path")
261
+ p.add_argument("-t", "--type")
262
+ p.add_argument("-e", "--edge-type")
263
+ p.set_defaults(func=cmd_query)
264
+
265
+ # benchmark
266
+ p = sub.add_parser("benchmark", help="Benchmark .md vs .llgr")
267
+ p.add_argument("path", help="Archivo o directorio .md")
268
+ p.add_argument("--no-recursive", action="store_true")
269
+ p.set_defaults(func=cmd_benchmark)
270
+
271
+ # diff
272
+ p = sub.add_parser("diff", help="Comparar dos grafos")
273
+ p.add_argument("graph1")
274
+ p.add_argument("graph2")
275
+ p.set_defaults(func=cmd_diff)
276
+
277
+ # info
278
+ p = sub.add_parser("info", help="Info de un archivo .llgr")
279
+ p.add_argument("path")
280
+ p.set_defaults(func=cmd_info)
281
+
282
+ # context
283
+ p = sub.add_parser("context", help="Gestionar contexto operativo para agentes")
284
+ csub = p.add_subparsers(dest="context_command")
285
+
286
+ cp = csub.add_parser("init", help="Inicializar context/ y context-policy.toml")
287
+ cp.add_argument("project", help="Ruta del proyecto")
288
+ cp.set_defaults(func=cmd_context_init)
289
+
290
+ cp = csub.add_parser("build", help="Compilar fuentes Markdown a context/project.llgr.zlib")
291
+ cp.add_argument("project", help="Ruta del proyecto")
292
+ cp.set_defaults(func=cmd_context_build)
293
+
294
+ cp = csub.add_parser("prepare", help="Generar Context Pack para una tarea")
295
+ cp.add_argument("project", help="Ruta del proyecto")
296
+ cp.add_argument("task", help="Tarea o pregunta")
297
+ cp.add_argument("--profile", default=None, help="Perfil: coding, debugging, review, planning, architecture, handoff")
298
+ cp.add_argument("--max-tokens", type=int, default=None)
299
+ cp.add_argument("--save", action="store_true", help="Guardar pack en context/packs/")
300
+ cp.add_argument("--json", action="store_true", help="Emitir JSON en vez de Markdown")
301
+ cp.set_defaults(func=cmd_context_prepare)
302
+
303
+ cp = csub.add_parser("search", help="Buscar dentro del grafo de contexto")
304
+ cp.add_argument("project", help="Ruta del proyecto")
305
+ cp.add_argument("query", help="Consulta")
306
+ cp.add_argument("--profile", default=None)
307
+ cp.add_argument("--top-k", type=int, default=10)
308
+ cp.set_defaults(func=cmd_context_search)
309
+
310
+ cp = csub.add_parser("health", help="Auditar el grafo de contexto")
311
+ cp.add_argument("project", help="Ruta del proyecto")
312
+ cp.set_defaults(func=cmd_context_health)
313
+
314
+ cp = csub.add_parser("update", help="Agregar memoria operativa al grafo")
315
+ cp.add_argument("project", help="Ruta del proyecto")
316
+ cp.add_argument("--type", default="note", help="Tipo de nodo a crear")
317
+ cp.add_argument("--source", default="agent-update")
318
+ cp.add_argument("note", help="Nota a agregar")
319
+ cp.set_defaults(func=cmd_context_update)
320
+
321
+ args = parser.parse_args()
322
+ if not args.command:
323
+ parser.print_help()
324
+ sys.exit(1)
325
+ if args.command == "context" and not args.context_command:
326
+ p.print_help()
327
+ sys.exit(1)
328
+ args.func(args)
329
+
330
+
331
+ if __name__ == "__main__":
332
+ main()
llmgraph/compressor.py ADDED
@@ -0,0 +1,100 @@
1
+ """
2
+ LLMGraph Compressor — Compresión adicional post-encoding (LZ4/zstd).
3
+
4
+ El formato binario ya deduplica strings, pero una capa de compresión
5
+ general puede ganar otro 2-4x en grafos grandes.
6
+ """
7
+ from __future__ import annotations
8
+ import struct
9
+ from .schema import MAGIC
10
+
11
+
12
+ # ── Compresión con zlib (stdlib, sin dependencias extra) ──
13
+ def compress_zlib(data: bytes, level: int = 6) -> bytes:
14
+ """Comprime datos con zlib (deflate). Buena compresión, rápido."""
15
+ import zlib
16
+ compressed = zlib.compress(data, level)
17
+ # Header: MAGIC + "ZL" + compressed_len(4B) + original_len(4B)
18
+ return MAGIC + b"ZL" + struct.pack("<II", len(compressed), len(data)) + compressed
19
+
20
+
21
+ def decompress_zlib(data: bytes) -> bytes:
22
+ """Descomprime datos comprimidos con compress_zlib."""
23
+ import zlib
24
+ if data[:4] != MAGIC or data[4:6] != b"ZL":
25
+ raise ValueError("No es un archivo LLMGraph comprimido con zlib")
26
+ comp_len, orig_len = struct.unpack_from("<II", data, 6)
27
+ compressed = data[14:14+comp_len]
28
+ return zlib.decompress(compressed)
29
+
30
+
31
+ # ── Compresión con LZ4 (si está disponible) ──
32
+ try:
33
+ import lz4.frame
34
+ HAS_LZ4 = True
35
+ except ImportError:
36
+ HAS_LZ4 = False
37
+
38
+
39
+ def compress_lz4(data: bytes) -> bytes:
40
+ if not HAS_LZ4:
41
+ raise ImportError("pip install lz4")
42
+ compressed = lz4.frame.compress(data)
43
+ return MAGIC + b"LZ4" + struct.pack("<II", len(compressed), len(data)) + compressed
44
+
45
+
46
+ def decompress_lz4(data: bytes) -> bytes:
47
+ if not HAS_LZ4:
48
+ raise ImportError("pip install lz4")
49
+ if data[:4] != MAGIC or data[4:7] != b"LZ4":
50
+ raise ValueError("No es un archivo LLMGraph comprimido con LZ4")
51
+ comp_len, orig_len = struct.unpack_from("<II", data, 7)
52
+ compressed = data[15:15+comp_len]
53
+ return lz4.frame.decompress(compressed)
54
+
55
+
56
+ # ── API unificada ──
57
+ def compress(data: bytes, method: str = "zlib", level: int = 6) -> bytes:
58
+ """Comprime un grafo .llgr con el método especificado.
59
+
60
+ method: "zlib" (default, stdlib) o "lz4" (requiere pip install lz4)
61
+ """
62
+ if method == "lz4":
63
+ return compress_lz4(data)
64
+ return compress_zlib(data, level)
65
+
66
+
67
+ def decompress(data: bytes) -> bytes:
68
+ """Auto-detecta el método y descomprime."""
69
+ if data[:4] != MAGIC:
70
+ raise ValueError("No es un archivo LLMGraph")
71
+ method = data[4:6] if len(data) > 5 else b""
72
+ if method == b"ZL":
73
+ return decompress_zlib(data)
74
+ elif data[4:7] == b"LZ4":
75
+ return decompress_lz4(data)
76
+ # No compression — return as-is
77
+ return data
78
+
79
+
80
+ def is_compressed(data: bytes) -> bool:
81
+ return data[:4] == MAGIC and ((len(data) > 6 and data[4:6] == b"ZL") or (len(data) > 7 and data[4:7] == b"LZ4"))
82
+
83
+
84
+ def stats(data: bytes) -> dict:
85
+ """Retorna estadísticas de compresión."""
86
+ if not is_compressed(data):
87
+ return {"compressed": False, "size": len(data)}
88
+ method = "lz4" if data[4:7] == b"LZ4" else "zlib"
89
+ if method == "zlib":
90
+ comp_len, orig_len = struct.unpack_from("<II", data, 6)
91
+ else:
92
+ comp_len, orig_len = struct.unpack_from("<II", data, 7)
93
+ return {
94
+ "compressed": True,
95
+ "method": method,
96
+ "compressed_size": len(data),
97
+ "original_size": orig_len,
98
+ "ratio": orig_len / max(len(data), 1),
99
+ "savings_pct": (1 - len(data) / max(orig_len, 1)) * 100,
100
+ }