Implements the processual architecture based on Whitehead's Process Philosophy and Peirce's Semiotics. Core paradigm: "L'espace latent pense. Le LLM traduit." (The latent space thinks. The LLM translates.) Phase 1-4: Core semiotic cycle - StateTensor 8x1024 (8 Peircean dimensions) - Dissonance computation with hard negatives - Fixation via 4 Peircean methods (Tenacity, Authority, A Priori, Science) - LatentEngine orchestrating the full cycle Phase 5: StateToLanguage - LLM as pure translator (zero-reasoning, T=0) - Projection on interpretable directions - Reasoning markers detection (Amendment #4) Phase 6: Vigilance - x_ref (David) as guard-rail, NOT attractor - Drift detection per dimension and globally - Alerts: ok, warning, critical Phase 7: Autonomous Daemon - Two modes: CONVERSATION (always verbalize), AUTONOMOUS (~1000 cycles/day) - Amendment #5: 50% probability on unresolved impacts - TriggerGenerator with weighted random selection Phase 8: Integration & Metrics - ProcessMetrics for daily/weekly reports - Health status monitoring - Integration tests validating all modules 297 tests passing, version 0.7.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
601 lines
21 KiB
Python
601 lines
21 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
StateTensor - Tenseur d'état 8×1024 d'Ikario v2.
|
||
|
||
Le tenseur d'état représente l'identité processuelle d'Ikario avec 8 dimensions :
|
||
- firstness : Qualia, saillances, possibles (Peirce)
|
||
- secondness : Chocs, tensions, irritations (Peirce)
|
||
- thirdness : Habitudes, positions, valeurs (Peirce)
|
||
- dispositions : Tendances à agir
|
||
- orientations : Vers quoi je tends
|
||
- engagements : Positions prises
|
||
- pertinences : Ce qui compte pour moi
|
||
- valeurs : Ce que je défends
|
||
|
||
Architecture: L'espace latent pense. Le LLM traduit.
|
||
"""
|
||
|
||
import os
|
||
from dataclasses import dataclass, field
|
||
from datetime import datetime
|
||
from enum import Enum
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
import numpy as np
|
||
import weaviate
|
||
import weaviate.classes.config as wvc
|
||
from weaviate.classes.query import Filter
|
||
|
||
# Configuration
|
||
WEAVIATE_URL = os.getenv("WEAVIATE_URL", "http://localhost:8080")
|
||
EMBEDDING_DIM = 1024 # BGE-M3
|
||
|
||
|
||
class TensorDimension(Enum):
|
||
"""Les 8 dimensions du tenseur d'état."""
|
||
FIRSTNESS = "firstness" # Qualia, saillances, possibles
|
||
SECONDNESS = "secondness" # Chocs, tensions, irritations
|
||
THIRDNESS = "thirdness" # Habitudes, positions, valeurs
|
||
DISPOSITIONS = "dispositions" # Tendances à agir
|
||
ORIENTATIONS = "orientations" # Vers quoi je tends
|
||
ENGAGEMENTS = "engagements" # Positions prises
|
||
PERTINENCES = "pertinences" # Ce qui compte pour moi
|
||
VALEURS = "valeurs" # Ce que je défends
|
||
|
||
|
||
DIMENSION_NAMES = [d.value for d in TensorDimension]
|
||
|
||
|
||
@dataclass
|
||
class StateTensor:
|
||
"""
|
||
Tenseur d'état X_t ∈ ℝ^(8×1024).
|
||
|
||
Chaque dimension est un vecteur BGE-M3 normalisé.
|
||
"""
|
||
state_id: int
|
||
timestamp: str
|
||
|
||
# Les 8 dimensions (chacune ∈ ℝ^1024)
|
||
firstness: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
|
||
secondness: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
|
||
thirdness: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
|
||
dispositions: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
|
||
orientations: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
|
||
engagements: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
|
||
pertinences: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
|
||
valeurs: np.ndarray = field(default_factory=lambda: np.zeros(EMBEDDING_DIM))
|
||
|
||
# Métadonnées
|
||
previous_state_id: int = -1
|
||
trigger_type: str = ""
|
||
trigger_content: str = ""
|
||
embedding_model: str = "BAAI/bge-m3" # Traçabilité (Amendement #13)
|
||
|
||
def to_matrix(self) -> np.ndarray:
|
||
"""Retourne le tenseur complet (8, 1024)."""
|
||
return np.stack([
|
||
self.firstness,
|
||
self.secondness,
|
||
self.thirdness,
|
||
self.dispositions,
|
||
self.orientations,
|
||
self.engagements,
|
||
self.pertinences,
|
||
self.valeurs
|
||
])
|
||
|
||
def to_flat(self) -> np.ndarray:
|
||
"""Retourne le tenseur aplati (8192,)."""
|
||
return self.to_matrix().flatten()
|
||
|
||
def get_dimension(self, dim: TensorDimension) -> np.ndarray:
|
||
"""Récupère une dimension par enum."""
|
||
return getattr(self, dim.value)
|
||
|
||
def set_dimension(self, dim: TensorDimension, vector: np.ndarray) -> None:
|
||
"""Définit une dimension par enum."""
|
||
if vector.shape != (EMBEDDING_DIM,):
|
||
raise ValueError(f"Vector must be {EMBEDDING_DIM}-dim, got {vector.shape}")
|
||
# Normaliser
|
||
norm = np.linalg.norm(vector)
|
||
if norm > 0:
|
||
vector = vector / norm
|
||
setattr(self, dim.value, vector)
|
||
|
||
def copy(self) -> 'StateTensor':
|
||
"""Crée une copie profonde."""
|
||
return StateTensor(
|
||
state_id=self.state_id,
|
||
timestamp=self.timestamp,
|
||
firstness=self.firstness.copy(),
|
||
secondness=self.secondness.copy(),
|
||
thirdness=self.thirdness.copy(),
|
||
dispositions=self.dispositions.copy(),
|
||
orientations=self.orientations.copy(),
|
||
engagements=self.engagements.copy(),
|
||
pertinences=self.pertinences.copy(),
|
||
valeurs=self.valeurs.copy(),
|
||
previous_state_id=self.previous_state_id,
|
||
trigger_type=self.trigger_type,
|
||
trigger_content=self.trigger_content,
|
||
embedding_model=self.embedding_model,
|
||
)
|
||
|
||
def to_dict(self) -> Dict[str, Any]:
|
||
"""Convertit en dictionnaire pour stockage."""
|
||
# S'assurer que le timestamp est au format RFC3339
|
||
ts = self.timestamp
|
||
if ts and not ts.endswith('Z') and '+' not in ts:
|
||
ts = ts + 'Z' # Ajouter le suffixe UTC si absent
|
||
|
||
return {
|
||
"state_id": self.state_id,
|
||
"timestamp": ts,
|
||
"previous_state_id": self.previous_state_id,
|
||
"trigger_type": self.trigger_type,
|
||
"trigger_content": self.trigger_content,
|
||
"embedding_model": self.embedding_model,
|
||
}
|
||
|
||
def get_vectors_dict(self) -> Dict[str, List[float]]:
|
||
"""Retourne les 8 vecteurs comme dict pour Weaviate named vectors."""
|
||
return {
|
||
"firstness": self.firstness.tolist(),
|
||
"secondness": self.secondness.tolist(),
|
||
"thirdness": self.thirdness.tolist(),
|
||
"dispositions": self.dispositions.tolist(),
|
||
"orientations": self.orientations.tolist(),
|
||
"engagements": self.engagements.tolist(),
|
||
"pertinences": self.pertinences.tolist(),
|
||
"valeurs": self.valeurs.tolist(),
|
||
}
|
||
|
||
@classmethod
|
||
def from_dict(cls, props: Dict[str, Any], vectors: Dict[str, List[float]] = None) -> 'StateTensor':
|
||
"""Crée un StateTensor depuis un dictionnaire (Weaviate object)."""
|
||
tensor = cls(
|
||
state_id=props.get("state_id", 0),
|
||
timestamp=props.get("timestamp", datetime.now().isoformat()),
|
||
previous_state_id=props.get("previous_state_id", -1),
|
||
trigger_type=props.get("trigger_type", ""),
|
||
trigger_content=props.get("trigger_content", ""),
|
||
embedding_model=props.get("embedding_model", "BAAI/bge-m3"),
|
||
)
|
||
|
||
if vectors:
|
||
for dim_name in DIMENSION_NAMES:
|
||
if dim_name in vectors:
|
||
setattr(tensor, dim_name, np.array(vectors[dim_name]))
|
||
|
||
return tensor
|
||
|
||
@classmethod
|
||
def from_matrix(cls, matrix: np.ndarray, state_id: int, timestamp: str) -> 'StateTensor':
|
||
"""Crée un StateTensor depuis une matrice (8, 1024)."""
|
||
if matrix.shape != (8, EMBEDDING_DIM):
|
||
raise ValueError(f"Matrix must be (8, {EMBEDDING_DIM}), got {matrix.shape}")
|
||
|
||
return cls(
|
||
state_id=state_id,
|
||
timestamp=timestamp,
|
||
firstness=matrix[0],
|
||
secondness=matrix[1],
|
||
thirdness=matrix[2],
|
||
dispositions=matrix[3],
|
||
orientations=matrix[4],
|
||
engagements=matrix[5],
|
||
pertinences=matrix[6],
|
||
valeurs=matrix[7],
|
||
)
|
||
|
||
@staticmethod
|
||
def weighted_mean(tensors: List['StateTensor'], weights: np.ndarray) -> 'StateTensor':
|
||
"""Calcule la moyenne pondérée de plusieurs tenseurs."""
|
||
if len(tensors) != len(weights):
|
||
raise ValueError("Number of tensors must match number of weights")
|
||
|
||
weights = np.array(weights) / np.sum(weights) # Normaliser
|
||
|
||
result = StateTensor(
|
||
state_id=-1, # À définir par l'appelant
|
||
timestamp=datetime.now().isoformat(),
|
||
)
|
||
|
||
for dim_name in DIMENSION_NAMES:
|
||
weighted_sum = np.zeros(EMBEDDING_DIM)
|
||
for tensor, weight in zip(tensors, weights):
|
||
weighted_sum += weight * getattr(tensor, dim_name)
|
||
# Normaliser le résultat
|
||
norm = np.linalg.norm(weighted_sum)
|
||
if norm > 0:
|
||
weighted_sum = weighted_sum / norm
|
||
setattr(result, dim_name, weighted_sum)
|
||
|
||
return result
|
||
|
||
@staticmethod
|
||
def blend(t1: 'StateTensor', t2: 'StateTensor', alpha: float = 0.5) -> 'StateTensor':
|
||
"""Mélange deux tenseurs : alpha * t1 + (1-alpha) * t2."""
|
||
return StateTensor.weighted_mean([t1, t2], [alpha, 1 - alpha])
|
||
|
||
|
||
# ============================================================================
|
||
# WEAVIATE COLLECTION SCHEMA (API v4)
|
||
# ============================================================================
|
||
|
||
def create_state_tensor_collection(client: weaviate.WeaviateClient) -> bool:
|
||
"""
|
||
Crée la collection StateTensor dans Weaviate avec 8 vecteurs nommés.
|
||
|
||
Utilise l'API Weaviate v4 avec named vectors.
|
||
|
||
Returns:
|
||
True si créée, False si existait déjà
|
||
"""
|
||
collection_name = "StateTensor"
|
||
|
||
# Vérifier si existe déjà
|
||
if collection_name in client.collections.list_all():
|
||
print(f"[StateTensor] Collection existe déjà")
|
||
return False
|
||
|
||
# Créer la collection avec 8 vecteurs nommés
|
||
client.collections.create(
|
||
name=collection_name,
|
||
description="Tenseur d'état 8×1024 - Identité processuelle d'Ikario v2",
|
||
|
||
# 8 vecteurs nommés (Weaviate v4 API)
|
||
vector_config={
|
||
"firstness": wvc.Configure.NamedVectors.none(
|
||
name="firstness",
|
||
vector_index_config=wvc.Configure.VectorIndex.hnsw(
|
||
distance_metric=wvc.VectorDistances.COSINE
|
||
),
|
||
),
|
||
"secondness": wvc.Configure.NamedVectors.none(
|
||
name="secondness",
|
||
vector_index_config=wvc.Configure.VectorIndex.hnsw(
|
||
distance_metric=wvc.VectorDistances.COSINE
|
||
),
|
||
),
|
||
"thirdness": wvc.Configure.NamedVectors.none(
|
||
name="thirdness",
|
||
vector_index_config=wvc.Configure.VectorIndex.hnsw(
|
||
distance_metric=wvc.VectorDistances.COSINE
|
||
),
|
||
),
|
||
"dispositions": wvc.Configure.NamedVectors.none(
|
||
name="dispositions",
|
||
vector_index_config=wvc.Configure.VectorIndex.hnsw(
|
||
distance_metric=wvc.VectorDistances.COSINE
|
||
),
|
||
),
|
||
"orientations": wvc.Configure.NamedVectors.none(
|
||
name="orientations",
|
||
vector_index_config=wvc.Configure.VectorIndex.hnsw(
|
||
distance_metric=wvc.VectorDistances.COSINE
|
||
),
|
||
),
|
||
"engagements": wvc.Configure.NamedVectors.none(
|
||
name="engagements",
|
||
vector_index_config=wvc.Configure.VectorIndex.hnsw(
|
||
distance_metric=wvc.VectorDistances.COSINE
|
||
),
|
||
),
|
||
"pertinences": wvc.Configure.NamedVectors.none(
|
||
name="pertinences",
|
||
vector_index_config=wvc.Configure.VectorIndex.hnsw(
|
||
distance_metric=wvc.VectorDistances.COSINE
|
||
),
|
||
),
|
||
"valeurs": wvc.Configure.NamedVectors.none(
|
||
name="valeurs",
|
||
vector_index_config=wvc.Configure.VectorIndex.hnsw(
|
||
distance_metric=wvc.VectorDistances.COSINE
|
||
),
|
||
),
|
||
},
|
||
|
||
# Propriétés (métadonnées)
|
||
properties=[
|
||
wvc.Property(
|
||
name="state_id",
|
||
data_type=wvc.DataType.INT,
|
||
description="Numéro séquentiel de l'état (0, 1, 2...)",
|
||
),
|
||
wvc.Property(
|
||
name="timestamp",
|
||
data_type=wvc.DataType.DATE,
|
||
description="Moment de création de cet état",
|
||
),
|
||
wvc.Property(
|
||
name="previous_state_id",
|
||
data_type=wvc.DataType.INT,
|
||
description="ID de l'état précédent (-1 pour X_0)",
|
||
),
|
||
wvc.Property(
|
||
name="trigger_type",
|
||
data_type=wvc.DataType.TEXT,
|
||
skip_vectorization=True,
|
||
description="Type: user, timer, event, initialization",
|
||
),
|
||
wvc.Property(
|
||
name="trigger_content",
|
||
data_type=wvc.DataType.TEXT,
|
||
skip_vectorization=True,
|
||
description="Contenu du déclencheur",
|
||
),
|
||
wvc.Property(
|
||
name="embedding_model",
|
||
data_type=wvc.DataType.TEXT,
|
||
skip_vectorization=True,
|
||
description="Modèle d'embedding utilisé (traçabilité)",
|
||
),
|
||
],
|
||
)
|
||
|
||
print(f"[StateTensor] Collection créée avec 8 vecteurs nommés")
|
||
return True
|
||
|
||
|
||
def delete_state_tensor_collection(client: weaviate.WeaviateClient) -> bool:
|
||
"""Supprime la collection StateTensor (pour reset)."""
|
||
try:
|
||
client.collections.delete("StateTensor")
|
||
print("[StateTensor] Collection supprimée")
|
||
return True
|
||
except Exception as e:
|
||
print(f"[StateTensor] Erreur suppression: {e}")
|
||
return False
|
||
|
||
|
||
# ============================================================================
|
||
# CRUD OPERATIONS
|
||
# ============================================================================
|
||
|
||
class StateTensorRepository:
|
||
"""
|
||
Repository pour les opérations CRUD sur StateTensor.
|
||
|
||
Utilise l'API Weaviate v4.
|
||
"""
|
||
|
||
def __init__(self, client: weaviate.WeaviateClient):
|
||
self.client = client
|
||
self.collection = client.collections.get("StateTensor")
|
||
|
||
def save(self, tensor: StateTensor) -> str:
|
||
"""
|
||
Sauvegarde un StateTensor dans Weaviate.
|
||
|
||
Returns:
|
||
UUID de l'objet créé
|
||
"""
|
||
result = self.collection.data.insert(
|
||
properties=tensor.to_dict(),
|
||
vector=tensor.get_vectors_dict(),
|
||
)
|
||
return str(result)
|
||
|
||
def get_by_state_id(self, state_id: int) -> Optional[StateTensor]:
|
||
"""Récupère un tenseur par son state_id."""
|
||
results = self.collection.query.fetch_objects(
|
||
filters=Filter.by_property("state_id").equal(state_id),
|
||
include_vector=True,
|
||
limit=1,
|
||
)
|
||
|
||
if not results.objects:
|
||
return None
|
||
|
||
obj = results.objects[0]
|
||
return StateTensor.from_dict(obj.properties, obj.vector)
|
||
|
||
def get_current(self) -> Optional[StateTensor]:
|
||
"""Récupère l'état le plus récent (state_id max)."""
|
||
from weaviate.classes.query import Sort
|
||
|
||
results = self.collection.query.fetch_objects(
|
||
sort=Sort.by_property("state_id", ascending=False),
|
||
include_vector=True,
|
||
limit=1,
|
||
)
|
||
|
||
if not results.objects:
|
||
return None
|
||
|
||
obj = results.objects[0]
|
||
return StateTensor.from_dict(obj.properties, obj.vector)
|
||
|
||
def get_current_state_id(self) -> int:
|
||
"""Retourne l'ID de l'état le plus récent (-1 si aucun)."""
|
||
current = self.get_current()
|
||
return current.state_id if current else -1
|
||
|
||
def get_history(self, limit: int = 10) -> List[StateTensor]:
|
||
"""Récupère les N derniers états."""
|
||
from weaviate.classes.query import Sort
|
||
|
||
results = self.collection.query.fetch_objects(
|
||
sort=Sort.by_property("state_id", ascending=False),
|
||
include_vector=True,
|
||
limit=limit,
|
||
)
|
||
|
||
return [
|
||
StateTensor.from_dict(obj.properties, obj.vector)
|
||
for obj in results.objects
|
||
]
|
||
|
||
def count(self) -> int:
|
||
"""Compte le nombre total d'états."""
|
||
result = self.collection.aggregate.over_all(total_count=True)
|
||
return result.total_count
|
||
|
||
|
||
# ============================================================================
|
||
# IMPACT COLLECTION (pour Secondness)
|
||
# ============================================================================
|
||
|
||
def create_impact_collection(client: weaviate.WeaviateClient) -> bool:
|
||
"""
|
||
Crée la collection Impact pour les événements de dissonance.
|
||
|
||
Un Impact représente un "choc" (Secondness) - une tension non résolue
|
||
qui demande à être intégrée.
|
||
"""
|
||
collection_name = "Impact"
|
||
|
||
if collection_name in client.collections.list_all():
|
||
print(f"[Impact] Collection existe déjà")
|
||
return False
|
||
|
||
client.collections.create(
|
||
name=collection_name,
|
||
description="Événements de dissonance (chocs, tensions) - Secondness",
|
||
|
||
# Vecteur unique pour l'impact
|
||
vectorizer_config=wvc.Configure.Vectorizer.none(),
|
||
vector_index_config=wvc.Configure.VectorIndex.hnsw(
|
||
distance_metric=wvc.VectorDistances.COSINE
|
||
),
|
||
|
||
properties=[
|
||
wvc.Property(
|
||
name="trigger_content",
|
||
data_type=wvc.DataType.TEXT,
|
||
description="Contenu déclencheur de l'impact",
|
||
),
|
||
wvc.Property(
|
||
name="trigger_type",
|
||
data_type=wvc.DataType.TEXT,
|
||
skip_vectorization=True,
|
||
description="Type: user, corpus, veille, internal",
|
||
),
|
||
wvc.Property(
|
||
name="dissonance_score",
|
||
data_type=wvc.DataType.NUMBER,
|
||
description="Score de dissonance E() [0-1]",
|
||
),
|
||
wvc.Property(
|
||
name="state_id_at_impact",
|
||
data_type=wvc.DataType.INT,
|
||
description="state_id au moment de l'impact",
|
||
),
|
||
wvc.Property(
|
||
name="dimensions_affected",
|
||
data_type=wvc.DataType.TEXT_ARRAY,
|
||
skip_vectorization=True,
|
||
description="Dimensions du tenseur affectées",
|
||
),
|
||
wvc.Property(
|
||
name="is_hard_negative",
|
||
data_type=wvc.DataType.BOOL,
|
||
description="True si contradiction détectée (NLI)",
|
||
),
|
||
wvc.Property(
|
||
name="resolved",
|
||
data_type=wvc.DataType.BOOL,
|
||
description="True si l'impact a été intégré",
|
||
),
|
||
wvc.Property(
|
||
name="resolution_state_id",
|
||
data_type=wvc.DataType.INT,
|
||
description="state_id où l'impact a été résolu",
|
||
),
|
||
wvc.Property(
|
||
name="timestamp",
|
||
data_type=wvc.DataType.DATE,
|
||
description="Moment de l'impact",
|
||
),
|
||
wvc.Property(
|
||
name="last_rumination",
|
||
data_type=wvc.DataType.DATE,
|
||
description="Dernière rumination (cooldown 24h - Amendement #9)",
|
||
),
|
||
],
|
||
)
|
||
|
||
print(f"[Impact] Collection créée")
|
||
return True
|
||
|
||
|
||
# ============================================================================
|
||
# SETUP ALL COLLECTIONS
|
||
# ============================================================================
|
||
|
||
def create_all_processual_collections(client: weaviate.WeaviateClient) -> Dict[str, bool]:
|
||
"""
|
||
Crée toutes les collections pour le système processuel v2.
|
||
|
||
Returns:
|
||
Dict avec le statut de chaque collection
|
||
"""
|
||
print("=" * 60)
|
||
print("Création des collections processuelles v2")
|
||
print("=" * 60)
|
||
|
||
results = {
|
||
"StateTensor": create_state_tensor_collection(client),
|
||
"Impact": create_impact_collection(client),
|
||
}
|
||
|
||
print("\n" + "=" * 60)
|
||
print("Resume:")
|
||
for name, created in results.items():
|
||
status = "[OK] Creee" if created else "[WARN] Existait deja"
|
||
print(f" {name}: {status}")
|
||
|
||
return results
|
||
|
||
|
||
# ============================================================================
|
||
# CLI
|
||
# ============================================================================
|
||
|
||
if __name__ == "__main__":
|
||
import argparse
|
||
|
||
parser = argparse.ArgumentParser(description="Gestion des collections StateTensor")
|
||
parser.add_argument("--create", action="store_true", help="Créer les collections")
|
||
parser.add_argument("--delete", action="store_true", help="Supprimer les collections")
|
||
parser.add_argument("--status", action="store_true", help="Afficher le statut")
|
||
|
||
args = parser.parse_args()
|
||
|
||
# Connexion Weaviate
|
||
client = weaviate.connect_to_local()
|
||
|
||
try:
|
||
if args.create:
|
||
create_all_processual_collections(client)
|
||
|
||
elif args.delete:
|
||
delete_state_tensor_collection(client)
|
||
try:
|
||
client.collections.delete("Impact")
|
||
print("[Impact] Collection supprimée")
|
||
except Exception:
|
||
pass
|
||
|
||
elif args.status:
|
||
collections = client.collections.list_all()
|
||
print("Collections existantes:")
|
||
for name in sorted(collections.keys()):
|
||
if name in ["StateTensor", "Impact"]:
|
||
print(f" [OK] {name}")
|
||
|
||
if "StateTensor" in collections:
|
||
repo = StateTensorRepository(client)
|
||
print(f"\nStateTensor: {repo.count()} états")
|
||
current = repo.get_current()
|
||
if current:
|
||
print(f" État actuel: X_{current.state_id} ({current.timestamp})")
|
||
|
||
else:
|
||
parser.print_help()
|
||
|
||
finally:
|
||
client.close()
|