albert-memory 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.
Files changed (74) hide show
  1. albert/__init__.py +3 -0
  2. albert/admin.py +532 -0
  3. albert/api.py +698 -0
  4. albert/automatic_memory_release.json +14 -0
  5. albert/benchmark.py +272 -0
  6. albert/benchmark_cli.py +37 -0
  7. albert/capture.py +1051 -0
  8. albert/capture_installer.py +1000 -0
  9. albert/capture_promotion.py +298 -0
  10. albert/capture_retention.py +72 -0
  11. albert/capture_sdk.py +558 -0
  12. albert/capture_security.py +79 -0
  13. albert/chunking.py +79 -0
  14. albert/classifier.py +263 -0
  15. albert/claude_adapter.py +357 -0
  16. albert/claude_lifecycle.py +418 -0
  17. albert/claude_package.json +19 -0
  18. albert/claude_plugin/.claude-plugin/plugin.json +11 -0
  19. albert/claude_plugin/.mcp.json +13 -0
  20. albert/claude_plugin/hooks/hooks.json +70 -0
  21. albert/clusters.py +140 -0
  22. albert/codex_adapter.py +308 -0
  23. albert/codex_hooks/hooks.json +49 -0
  24. albert/codex_lifecycle.py +485 -0
  25. albert/codex_package.json +19 -0
  26. albert/compatibility.py +121 -0
  27. albert/config.py +144 -0
  28. albert/console.py +508 -0
  29. albert/console_backends.py +236 -0
  30. albert/console_map.py +518 -0
  31. albert/console_ops.py +919 -0
  32. albert/credentials.py +616 -0
  33. albert/cursor_adapter.py +320 -0
  34. albert/cursor_hooks/hooks.json +11 -0
  35. albert/cursor_rules/albert-memory.mdc +11 -0
  36. albert/db.py +68 -0
  37. albert/embeddings.py +91 -0
  38. albert/graph.py +623 -0
  39. albert/graph_builders.py +879 -0
  40. albert/graph_cypher.py +669 -0
  41. albert/graph_store.py +376 -0
  42. albert/host_targets/claude-code.json +45 -0
  43. albert/host_targets/codex.json +45 -0
  44. albert/host_targets/openclaw.json +45 -0
  45. albert/host_targets.py +271 -0
  46. albert/links.py +428 -0
  47. albert/mcp_server.py +457 -0
  48. albert/memory_safety.py +111 -0
  49. albert/models.py +611 -0
  50. albert/openclaw_adapter.py +326 -0
  51. albert/openclaw_adapter_trust.json +5 -0
  52. albert/openclaw_package.json +22 -0
  53. albert/openclaw_plugin/index.js +135 -0
  54. albert/openclaw_plugin/openclaw.plugin.json +25 -0
  55. albert/openclaw_plugin/package.json +12 -0
  56. albert/portability.py +291 -0
  57. albert/provenance.py +105 -0
  58. albert/recorder.py +166 -0
  59. albert/release_gate.py +83 -0
  60. albert/schemas.py +617 -0
  61. albert/scope.py +26 -0
  62. albert/search.py +447 -0
  63. albert/security.py +150 -0
  64. albert/services.py +1316 -0
  65. albert/session_context.py +162 -0
  66. albert/trace_feed.py +165 -0
  67. albert/trace_writer.py +131 -0
  68. albert/tracing.py +75 -0
  69. albert/worker.py +202 -0
  70. albert/workspaces.py +256 -0
  71. albert_memory-0.4.0.dist-info/METADATA +554 -0
  72. albert_memory-0.4.0.dist-info/RECORD +74 -0
  73. albert_memory-0.4.0.dist-info/WHEEL +4 -0
  74. albert_memory-0.4.0.dist-info/entry_points.txt +14 -0
albert/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Albert independent agent-memory service."""
2
+
3
+ __version__ = "0.4.0"
albert/admin.py ADDED
@@ -0,0 +1,532 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from datetime import UTC, datetime
6
+ from typing import Any, NoReturn
7
+ from uuid import UUID
8
+
9
+ import typer
10
+ from sqlalchemy import Engine, inspect, or_, select, text
11
+
12
+ from albert import __version__
13
+ from albert.db import SessionLocal, engine, initialize_database
14
+ from albert.embeddings import get_embedder
15
+ from albert.models import APIKey, Job, Memory, Organization, Principal, Workspace
16
+ from albert.security import ALL_CAPABILITIES, KNOWN_CAPABILITIES, issue_api_key
17
+
18
+ app = typer.Typer(help="Albert administrative commands", no_args_is_help=True)
19
+
20
+ # Version of the machine-readable `--json` output. Changes within a version are
21
+ # additive only; see contracts/admin-v1.schema.json.
22
+ ADMIN_CONTRACT_VERSION = "1"
23
+ JSON_COMMANDS = frozenset(
24
+ {
25
+ "contract",
26
+ "create-key",
27
+ "create-principal",
28
+ "create-workspace",
29
+ "list-keys",
30
+ "list-principals",
31
+ "revoke-key",
32
+ }
33
+ )
34
+ JSON_OPTION = typer.Option(
35
+ False, "--json", help="Print one machine-readable JSON object (admin contract)"
36
+ )
37
+
38
+
39
+ def _emit(payload: dict[str, Any]) -> None:
40
+ typer.echo(
41
+ json.dumps({"admin_contract": ADMIN_CONTRACT_VERSION, **payload}, separators=(",", ":"))
42
+ )
43
+
44
+
45
+ def _fail(json_mode: bool, code: str, message: str) -> NoReturn:
46
+ """Exit with code 2: a JSON error on stderr, or typer's usual BadParameter."""
47
+ if not json_mode:
48
+ raise typer.BadParameter(message)
49
+ error = {"admin_contract": ADMIN_CONTRACT_VERSION, "error": {"code": code, "message": message}}
50
+ typer.echo(json.dumps(error, separators=(",", ":")), err=True)
51
+ raise typer.Exit(code=2)
52
+
53
+
54
+ def _parse_id(value: str, json_mode: bool) -> UUID:
55
+ try:
56
+ return UUID(value)
57
+ except ValueError:
58
+ if not json_mode:
59
+ raise # Text mode keeps its historical behavior: an uncaught ValueError.
60
+ _fail(json_mode, "invalid_id", f"Invalid id: {value!r}")
61
+
62
+
63
+ def _timestamp(value: datetime | None) -> str | None:
64
+ if value is None:
65
+ return None
66
+ if value.tzinfo is None: # SQLite drops the offset; stored values are UTC.
67
+ value = value.replace(tzinfo=UTC)
68
+ return value.astimezone(UTC).isoformat()
69
+
70
+
71
+ def _workspace_json(workspace: Workspace) -> dict[str, Any]:
72
+ return {
73
+ "id": str(workspace.id),
74
+ "organization_id": str(workspace.organization_id),
75
+ "name": workspace.name,
76
+ }
77
+
78
+
79
+ def _principal_json(principal: Principal) -> dict[str, Any]:
80
+ return {
81
+ "id": str(principal.id),
82
+ "organization_id": str(principal.organization_id),
83
+ "name": principal.name,
84
+ "principal_type": principal.principal_type,
85
+ "workspace_id": str(principal.workspace_id) if principal.workspace_id else None,
86
+ "active": principal.active,
87
+ }
88
+
89
+
90
+ def _key_json(key: APIKey) -> dict[str, Any]:
91
+ # Never includes key_hash or any credential material.
92
+ return {
93
+ "id": str(key.id),
94
+ "principal_id": str(key.principal_id),
95
+ "name": key.name,
96
+ "prefix": key.prefix,
97
+ "capabilities": list(key.capabilities),
98
+ "active": key.active,
99
+ "expires_at": _timestamp(key.expires_at),
100
+ "created_at": _timestamp(key.created_at),
101
+ }
102
+
103
+
104
+ @app.command("compat")
105
+ def compat(
106
+ json_output: bool = typer.Option(
107
+ True, "--json", help="Print JSON (the only format; accepted for symmetry)"
108
+ ),
109
+ ) -> None:
110
+ """Print this image's runtime compatibility manifest as compact JSON.
111
+
112
+ Needs no environment and no database. Consumers decide rollback with: a
113
+ Brain migrated by image T can run image O iff every revision in
114
+ T.migrations.heads is in O.migrations.known_revisions.
115
+ """
116
+ from albert.compatibility import (
117
+ MigrationsUnavailable,
118
+ compatibility_error,
119
+ compatibility_manifest,
120
+ )
121
+
122
+ try:
123
+ manifest = compatibility_manifest()
124
+ except MigrationsUnavailable as exc:
125
+ error = compatibility_error("migrations_unavailable", str(exc))
126
+ sys.stderr.write(json.dumps(error, separators=(",", ":")) + "\n")
127
+ raise typer.Exit(code=2) from exc
128
+ sys.stdout.write(json.dumps(manifest, separators=(",", ":")) + "\n")
129
+
130
+
131
+ class SchemaNotReady(RuntimeError):
132
+ pass
133
+
134
+
135
+ def ensure_schema_ready(engine: Engine) -> None:
136
+ """Refuse to run against a database Alembic has not built.
137
+
138
+ Creating tables here would leave no alembic_version row and make the next
139
+ `alembic upgrade head` fail on tables that already exist.
140
+ """
141
+ if not inspect(engine).has_table("memories"):
142
+ raise SchemaNotReady(
143
+ "Database schema is missing; run `alembic upgrade head` before this command"
144
+ )
145
+
146
+
147
+ @app.command("init-db")
148
+ def init_db() -> None:
149
+ """Create tables directly for throwaway development databases only.
150
+
151
+ Deployed databases must use `alembic upgrade head`; this command records no
152
+ migration version.
153
+ """
154
+ initialize_database()
155
+ typer.echo("Database initialized (development only; no Alembic version recorded)")
156
+
157
+
158
+ @app.command()
159
+ def bootstrap(
160
+ organization: str = typer.Option(..., help="Initial organization name"),
161
+ workspace: str = typer.Option(..., help="Initial workspace name"),
162
+ principal: str = typer.Option("Administrator", help="Administrator display name"),
163
+ ) -> None:
164
+ """Create the first tenant and print its administrator key exactly once."""
165
+ try:
166
+ ensure_schema_ready(engine)
167
+ except SchemaNotReady as exc:
168
+ raise typer.BadParameter(str(exc)) from exc
169
+ with SessionLocal() as session:
170
+ org = session.scalar(select(Organization).where(Organization.name == organization))
171
+ if org is None:
172
+ org = Organization(name=organization)
173
+ session.add(org)
174
+ session.flush()
175
+ work = session.scalar(
176
+ select(Workspace).where(
177
+ Workspace.organization_id == org.id, Workspace.name == workspace
178
+ )
179
+ )
180
+ if work is None:
181
+ work = Workspace(organization_id=org.id, name=workspace)
182
+ session.add(work)
183
+ session.flush()
184
+ actor = session.scalar(
185
+ select(Principal).where(
186
+ Principal.organization_id == org.id,
187
+ Principal.workspace_id == work.id,
188
+ Principal.name == principal,
189
+ )
190
+ )
191
+ if actor is None:
192
+ actor = Principal(
193
+ organization_id=org.id,
194
+ workspace_id=work.id,
195
+ name=principal,
196
+ principal_type="human",
197
+ )
198
+ session.add(actor)
199
+ session.flush()
200
+ raw, prefix, digest = issue_api_key()
201
+ session.add(
202
+ APIKey(
203
+ principal_id=actor.id,
204
+ prefix=prefix,
205
+ key_hash=digest,
206
+ name="bootstrap administrator",
207
+ capabilities=ALL_CAPABILITIES,
208
+ )
209
+ )
210
+ session.commit()
211
+ typer.echo("Bootstrap complete. Store this key securely; it will not be shown again:")
212
+ typer.echo(raw)
213
+
214
+
215
+ @app.command("create-key")
216
+ def create_key(
217
+ principal_id: str = typer.Option(...),
218
+ name: str = typer.Option("agent key"),
219
+ capabilities: str = typer.Option("memory.read,memory.write,graph.query"),
220
+ json_mode: bool = JSON_OPTION,
221
+ ) -> None:
222
+ """Issue an additional API key for an existing principal."""
223
+ requested_capabilities = [item.strip() for item in capabilities.split(",") if item.strip()]
224
+ unknown = sorted(set(requested_capabilities) - KNOWN_CAPABILITIES)
225
+ if unknown:
226
+ _fail(json_mode, "unknown_capabilities", f"Unknown capabilities: {', '.join(unknown)}")
227
+ with SessionLocal() as session:
228
+ actor = session.get(Principal, _parse_id(principal_id, json_mode))
229
+ if actor is None:
230
+ _fail(json_mode, "principal_not_found", "Principal not found")
231
+ raw, prefix, digest = issue_api_key()
232
+ key = APIKey(
233
+ principal_id=actor.id,
234
+ prefix=prefix,
235
+ key_hash=digest,
236
+ name=name,
237
+ capabilities=requested_capabilities,
238
+ )
239
+ session.add(key)
240
+ session.commit()
241
+ if json_mode:
242
+ session.refresh(key)
243
+ _emit({"key": _key_json(key), "credential": raw})
244
+ return
245
+ typer.echo(raw)
246
+
247
+
248
+ @app.command("create-workspace")
249
+ def create_workspace(
250
+ organization: str = typer.Option(..., help="Organization name"),
251
+ name: str = typer.Option(..., help="Workspace name"),
252
+ json_mode: bool = JSON_OPTION,
253
+ ) -> None:
254
+ """Create a workspace in an existing organization."""
255
+ with SessionLocal() as session:
256
+ org = session.scalar(select(Organization).where(Organization.name == organization))
257
+ if org is None:
258
+ _fail(json_mode, "organization_not_found", "Organization not found")
259
+ existing = session.scalar(
260
+ select(Workspace).where(
261
+ Workspace.organization_id == org.id,
262
+ Workspace.name == name,
263
+ )
264
+ )
265
+ if existing is not None:
266
+ if json_mode:
267
+ _emit({"workspace": _workspace_json(existing), "created": False})
268
+ else:
269
+ typer.echo(str(existing.id))
270
+ return
271
+ workspace = Workspace(organization_id=org.id, name=name)
272
+ session.add(workspace)
273
+ session.commit()
274
+ session.refresh(workspace)
275
+ if json_mode:
276
+ _emit({"workspace": _workspace_json(workspace), "created": True})
277
+ else:
278
+ typer.echo(str(workspace.id))
279
+
280
+
281
+ @app.command("create-principal")
282
+ def create_principal(
283
+ organization: str = typer.Option(..., help="Organization name"),
284
+ name: str = typer.Option(..., help="Principal display name"),
285
+ workspace: str | None = typer.Option(None, help="Optional workspace name"),
286
+ principal_type: str = typer.Option("agent", help="agent, human, or service"),
287
+ json_mode: bool = JSON_OPTION,
288
+ ) -> None:
289
+ """Create an organization- or workspace-scoped principal."""
290
+ if principal_type not in {"agent", "human", "service"}:
291
+ _fail(
292
+ json_mode,
293
+ "invalid_principal_type",
294
+ "principal-type must be agent, human, or service",
295
+ )
296
+ with SessionLocal() as session:
297
+ org = session.scalar(select(Organization).where(Organization.name == organization))
298
+ if org is None:
299
+ _fail(json_mode, "organization_not_found", "Organization not found")
300
+ workspace_id = None
301
+ if workspace is not None:
302
+ record = session.scalar(
303
+ select(Workspace).where(
304
+ Workspace.organization_id == org.id,
305
+ Workspace.name == workspace,
306
+ )
307
+ )
308
+ if record is None:
309
+ _fail(json_mode, "workspace_not_found", "Workspace not found")
310
+ workspace_id = record.id
311
+ principal = Principal(
312
+ organization_id=org.id,
313
+ workspace_id=workspace_id,
314
+ name=name,
315
+ principal_type=principal_type,
316
+ )
317
+ session.add(principal)
318
+ session.commit()
319
+ session.refresh(principal)
320
+ if json_mode:
321
+ _emit({"principal": _principal_json(principal)})
322
+ else:
323
+ typer.echo(str(principal.id))
324
+
325
+
326
+ @app.command("list-principals")
327
+ def list_principals(
328
+ organization: str = typer.Option(..., help="Organization name"),
329
+ json_mode: bool = JSON_OPTION,
330
+ ) -> None:
331
+ """List principals and their workspace scope."""
332
+ with SessionLocal() as session:
333
+ org = session.scalar(select(Organization).where(Organization.name == organization))
334
+ if org is None:
335
+ _fail(json_mode, "organization_not_found", "Organization not found")
336
+ principals = session.scalars(
337
+ select(Principal).where(Principal.organization_id == org.id).order_by(Principal.name)
338
+ )
339
+ if json_mode:
340
+ _emit(
341
+ {
342
+ "organization": {"id": str(org.id), "name": org.name},
343
+ "principals": [_principal_json(principal) for principal in principals],
344
+ }
345
+ )
346
+ return
347
+ for principal in principals:
348
+ typer.echo(
349
+ f"{principal.id}\t{principal.name}\t{principal.principal_type}\t"
350
+ f"workspace={principal.workspace_id or '*'}\tactive={principal.active}"
351
+ )
352
+
353
+
354
+ @app.command("list-keys")
355
+ def list_keys(
356
+ principal_id: str = typer.Option(...),
357
+ json_mode: bool = JSON_OPTION,
358
+ ) -> None:
359
+ """List non-secret API-key metadata for one principal."""
360
+ with SessionLocal() as session:
361
+ actor = session.get(Principal, _parse_id(principal_id, json_mode))
362
+ if actor is None:
363
+ _fail(json_mode, "principal_not_found", "Principal not found")
364
+ keys = session.scalars(
365
+ select(APIKey).where(APIKey.principal_id == actor.id).order_by(APIKey.created_at)
366
+ )
367
+ if json_mode:
368
+ _emit({"principal_id": str(actor.id), "keys": [_key_json(key) for key in keys]})
369
+ return
370
+ for key in keys:
371
+ typer.echo(
372
+ f"{key.id}\t{key.name}\tprefix={key.prefix}\tactive={key.active}\t"
373
+ f"expires={key.expires_at or '-'}"
374
+ )
375
+
376
+
377
+ @app.command("revoke-key")
378
+ def revoke_key(
379
+ key_id: str = typer.Option(...),
380
+ json_mode: bool = JSON_OPTION,
381
+ ) -> None:
382
+ """Revoke an API key without deleting its audit identity."""
383
+ with SessionLocal() as session:
384
+ key = session.get(APIKey, _parse_id(key_id, json_mode))
385
+ if key is None:
386
+ _fail(json_mode, "api_key_not_found", "API key not found")
387
+ key.active = False
388
+ session.commit()
389
+ if json_mode:
390
+ _emit({"key": {"id": str(key.id), "active": key.active}})
391
+ return
392
+ typer.echo("API key revoked")
393
+
394
+
395
+ @app.command("contract")
396
+ def contract(json_mode: bool = JSON_OPTION) -> None:
397
+ """Print the machine-readable admin contract version (always JSON)."""
398
+ _emit(
399
+ {
400
+ "package": "albert-memory",
401
+ "package_version": __version__,
402
+ "json_commands": sorted(JSON_COMMANDS),
403
+ }
404
+ )
405
+
406
+
407
+ @app.command("validate-runtime")
408
+ def validate_runtime(
409
+ probe_embedding: bool = typer.Option(
410
+ False, "--probe-embedding", help="Load and execute the configured embedding provider"
411
+ ),
412
+ ) -> None:
413
+ """Validate configured providers and optionally execute one embedding."""
414
+ from albert.config import get_settings
415
+ from albert.graph_builders import get_graph_builder
416
+ from albert.graph_store import get_graph_store
417
+
418
+ settings = get_settings()
419
+ typer.echo(
420
+ f"embedding={settings.embedding_provider}:{settings.embedding_model} "
421
+ f"dimensions={settings.embedding_dimensions} classifier={settings.llm_provider}"
422
+ )
423
+ with SessionLocal() as session:
424
+ store_version = get_graph_store().ping(session)
425
+ typer.echo(f"graph store probe succeeded ({store_version})")
426
+ if session.bind is not None and session.bind.dialect.name == "postgresql":
427
+ vector_type = session.scalar(
428
+ text(
429
+ "SELECT format_type(attribute.atttypid, attribute.atttypmod) "
430
+ "FROM pg_attribute AS attribute "
431
+ "JOIN pg_class AS relation ON relation.oid = attribute.attrelid "
432
+ "WHERE relation.relname = 'memory_chunks' "
433
+ "AND attribute.attname = 'embedding' "
434
+ "AND NOT attribute.attisdropped"
435
+ )
436
+ )
437
+ expected = f"vector({settings.embedding_dimensions})"
438
+ if vector_type != expected:
439
+ typer.echo(
440
+ f"database embedding type is {vector_type!r}; expected {expected!r}",
441
+ err=True,
442
+ )
443
+ raise typer.Exit(code=1)
444
+ hnsw_exists = session.scalar(
445
+ text(
446
+ "SELECT EXISTS (SELECT 1 FROM pg_indexes "
447
+ "WHERE indexname = 'ix_memory_chunks_embedding_hnsw')"
448
+ )
449
+ )
450
+ if not hnsw_exists:
451
+ typer.echo("database HNSW embedding index is missing", err=True)
452
+ raise typer.Exit(code=1)
453
+ typer.echo("database vector schema and HNSW index are valid")
454
+ if probe_embedding:
455
+ try:
456
+ embedder = get_embedder()
457
+ vector = embedder.embed("Albert runtime readiness probe")
458
+ except Exception as exc:
459
+ typer.echo(f"embedding probe failed ({type(exc).__name__})", err=True)
460
+ raise typer.Exit(code=1) from exc
461
+ if len(vector) != settings.embedding_dimensions:
462
+ raise typer.Exit(code=1)
463
+ typer.echo(f"embedding probe succeeded ({len(vector)} dimensions)")
464
+ if settings.graph_builder == "graphiti":
465
+ try:
466
+ builder_version = get_graph_builder().readiness()
467
+ except Exception as exc:
468
+ typer.echo(f"graph builder probe failed ({type(exc).__name__})", err=True)
469
+ raise typer.Exit(code=1) from exc
470
+ typer.echo(f"graph builder probe succeeded (graphiti-core {builder_version})")
471
+
472
+
473
+ @app.command("reindex-memories")
474
+ def reindex_memories(
475
+ all_memories: bool = typer.Option(
476
+ False, "--all", help="Re-enrich every active memory, including current embeddings"
477
+ ),
478
+ workspace: str | None = typer.Option(
479
+ None, help="Limit to workspaces with this name (in every organization)"
480
+ ),
481
+ ) -> None:
482
+ """Queue active memories whose chunks are absent or embedded by another model."""
483
+ embedder = get_embedder()
484
+ with SessionLocal() as session:
485
+ statement = select(Memory).where(Memory.status == "active")
486
+ if not all_memories:
487
+ statement = statement.where(
488
+ or_(Memory.embedding_model.is_(None), Memory.embedding_model != embedder.name)
489
+ )
490
+ if workspace is not None:
491
+ workspace_ids = list(
492
+ session.scalars(select(Workspace.id).where(Workspace.name == workspace))
493
+ )
494
+ if not workspace_ids:
495
+ raise typer.BadParameter("Workspace not found")
496
+ statement = statement.where(Memory.workspace_id.in_(workspace_ids))
497
+ selected = list(session.scalars(statement))
498
+ for memory in selected:
499
+ session.add(
500
+ Job(
501
+ organization_id=memory.organization_id,
502
+ job_type="enrich_memory",
503
+ payload={"memory_id": str(memory.id)},
504
+ )
505
+ )
506
+ session.commit()
507
+ typer.echo(f"queued {len(selected)} memories for enrichment with {embedder.name}")
508
+
509
+
510
+ @app.command("rebuild-links")
511
+ def rebuild_links_command(
512
+ organization: str | None = typer.Option(None, help="Limit to one organization by name"),
513
+ ) -> None:
514
+ """Recompute the memory map's similarity and sequence links from stored chunks."""
515
+ from albert.links import rebuild_links
516
+
517
+ with SessionLocal() as session:
518
+ organization_id = None
519
+ if organization is not None:
520
+ org = session.scalar(select(Organization).where(Organization.name == organization))
521
+ if org is None:
522
+ raise typer.BadParameter("Organization not found")
523
+ organization_id = org.id
524
+ result = rebuild_links(session, organization_id)
525
+ session.commit()
526
+ typer.echo(
527
+ f"memories={result['memories']} similar={result['similar']} sequence={result['sequence']}"
528
+ )
529
+
530
+
531
+ if __name__ == "__main__":
532
+ app()