clsplusplus 7.2.4__tar.gz → 7.3.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.
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/PKG-INFO +10 -1
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/pyproject.toml +35 -1
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/api.py +244 -1
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/config.py +13 -0
- clsplusplus-7.3.0/src/clsplusplus/embeddings.py +173 -0
- clsplusplus-7.3.0/src/clsplusplus/integrations/__init__.py +46 -0
- clsplusplus-7.3.0/src/clsplusplus/integrations/autogen.py +283 -0
- clsplusplus-7.3.0/src/clsplusplus/integrations/crewai.py +183 -0
- clsplusplus-7.3.0/src/clsplusplus/integrations/langgraph.py +330 -0
- clsplusplus-7.3.0/src/clsplusplus/integrations/llamaindex.py +253 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/local_routes.py +6 -9
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/memory_phase.py +94 -15
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/memory_service.py +92 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/middleware.py +2 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/models.py +3 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/oauth_server.py +62 -1
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/oauth_store.py +9 -9
- clsplusplus-7.3.0/src/clsplusplus/write_metrics.py +74 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus.egg-info/PKG-INFO +10 -1
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus.egg-info/SOURCES.txt +13 -1
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus.egg-info/requires.txt +13 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_admin.py +54 -0
- clsplusplus-7.3.0/tests/test_autogen_memory.py +350 -0
- clsplusplus-7.3.0/tests/test_crewai_provider.py +161 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_embeddings.py +112 -8
- clsplusplus-7.3.0/tests/test_integration_sync_cli_contract.py +129 -0
- clsplusplus-7.3.0/tests/test_langgraph_store.py +341 -0
- clsplusplus-7.3.0/tests/test_llamaindex_block.py +288 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_mcp_oauth.py +53 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_memory_phase.py +75 -25
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_reconsolidation.py +130 -1
- clsplusplus-7.3.0/tests/test_write_metrics.py +100 -0
- clsplusplus-7.2.4/src/clsplusplus/embeddings.py +0 -52
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/LICENSE +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/README.md +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/setup.cfg +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/__init__.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/abuse_guard.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/api_usage_analytics.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/auth.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/cli.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/client.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/cost_forecast.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/cost_model.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/debug_console.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/demo_llm.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/demo_llm_calls.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/demo_local.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/email_service.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/funnel_routes.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/geo.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/health_metrics.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/homepage_autopromote.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/idempotency.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/integration_service.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/jwt_utils.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/main.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/mcp_http.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/mcp_server.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/memory_cycle.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/metering_v2/__init__.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/metering_v2/__main__.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/metering_v2/billing.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/metering_v2/healthcheck.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/metering_v2/notifier.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/metering_v2/pricing.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/metering_v2/reconciler.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/metering_v2/schema.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/metering_v2/writer.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/metrics.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/namespace_resolver.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/permissions.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/plasticity.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/pricing.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/pricing_models.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/pricing_store.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/prompt_log.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/rate_limit.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/razorpay_service.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/rbac_service.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/reconsolidation.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/resilience.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/sleep_cycle.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/__init__.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/base.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/chat_session_store.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/integration_store.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/l0_working_buffer.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/l1_indexing_store.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/l2_schema_graph.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/l3_deep_recess.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/l3_postgres.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/rbac_store.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/user_store.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/waitlist_store.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stores/web_events_store.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/stripe_service.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/subscription_watchdog.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/temporal.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/test_suite.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/tier_resolver.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/tiers.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/topical_resonance.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/tracer.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/usage.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/user_embeddings.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/user_pulse.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/user_service.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/waitlist_service.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/webhook_dispatcher.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/weblab.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/weblab_watcher.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus/window_limits.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus.egg-info/dependency_links.txt +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus.egg-info/entry_points.txt +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/src/clsplusplus.egg-info/top_level.txt +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_abuse_guard.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_admin_seed.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_api.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_api_comprehensive.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_api_endpoints.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_api_usage_analytics.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_auth.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_auth_me_api_key.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_billing_e2e.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_burst_hardening.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_client_sdk.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_config.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_cost_forecast.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_cross_llm_memory.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_debug_console.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_demo_llm.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_extension_integration.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_extension_ui.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_feedback_github.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_free_tier_lifecycle.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_full_api_coverage.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_funnel_metrics.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_geo_gating.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_health_metrics.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_idempotency.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_integration_service.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_mcp_connect.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_memory_cycle.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_memory_service.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_memory_write_no_schema_garbage.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_metering_v2_healthcheck.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_metering_v2_notifier.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_metering_v2_pricing.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_metering_v2_reconciler.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_metering_v2_schema.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_metering_v2_writer.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_middleware.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_models.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_overage_billing.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_performance.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_plasticity.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_pricing.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_prototype_e2e.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_rate_limit.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_razorpay_billing.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_razorpay_subscription_webhooks.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_regression.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_resilience.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_security.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_sleep_cycle.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_stores.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_subscription_watchdog.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_tier_resolver.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_tiers.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_usage.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_user_auth.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_user_embeddings.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_user_pulse.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_user_stories.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_waitlist.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_webhook_dispatcher.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_weblab.py +0 -0
- {clsplusplus-7.2.4 → clsplusplus-7.3.0}/tests/test_window_limits.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clsplusplus
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.3.0
|
|
4
4
|
Summary: Brain-inspired, model-agnostic persistent memory for LLMs. Learn, recall, forget — like a brain. Works with OpenAI, Claude, Gemini, Llama.
|
|
5
5
|
Author-email: AlphaForge AI Labs <contact@alphaforge.ai>
|
|
6
6
|
Maintainer-email: Rajamohan Jabbala <contact@alphaforge.ai>
|
|
@@ -52,6 +52,15 @@ Requires-Dist: razorpay>=1.4.0; extra == "server"
|
|
|
52
52
|
Requires-Dist: minio>=7.2.0; extra == "server"
|
|
53
53
|
Requires-Dist: pyarrow>=14.0.0; extra == "server"
|
|
54
54
|
Requires-Dist: posthog>=3.5.0; extra == "server"
|
|
55
|
+
Provides-Extra: crewai
|
|
56
|
+
Requires-Dist: crewai>=1.0.0; extra == "crewai"
|
|
57
|
+
Provides-Extra: langgraph
|
|
58
|
+
Requires-Dist: langgraph>=1.0.0; extra == "langgraph"
|
|
59
|
+
Provides-Extra: llamaindex
|
|
60
|
+
Requires-Dist: llama-index-core>=0.12.0; extra == "llamaindex"
|
|
61
|
+
Provides-Extra: autogen
|
|
62
|
+
Requires-Dist: autogen-core>=0.4.0; extra == "autogen"
|
|
63
|
+
Requires-Dist: autogen-agentchat>=0.4.0; extra == "autogen"
|
|
55
64
|
Provides-Extra: dev
|
|
56
65
|
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
57
66
|
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "clsplusplus"
|
|
7
|
-
version = "7.
|
|
7
|
+
version = "7.3.0"
|
|
8
8
|
description = "Brain-inspired, model-agnostic persistent memory for LLMs. Learn, recall, forget — like a brain. Works with OpenAI, Claude, Gemini, Llama."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -63,6 +63,40 @@ server = [
|
|
|
63
63
|
"pyarrow>=14.0.0",
|
|
64
64
|
"posthog>=3.5.0",
|
|
65
65
|
]
|
|
66
|
+
# CrewAI integration — pip install clsplusplus[crewai]
|
|
67
|
+
# Provides clsplusplus.integrations.crewai.CLSMemoryStorage, a native CrewAI
|
|
68
|
+
# external-memory backend. Floor is the CrewAI 1.x line that ships the stable
|
|
69
|
+
# crewai.memory.storage.interface.Storage + ExternalMemory API this adapts to.
|
|
70
|
+
crewai = [
|
|
71
|
+
"crewai>=1.0.0",
|
|
72
|
+
]
|
|
73
|
+
# LangGraph integration — pip install clsplusplus[langgraph]
|
|
74
|
+
# Provides clsplusplus.integrations.langgraph.CLSMemoryStore, a native
|
|
75
|
+
# subclass of langgraph.store.base.BaseStore (the long-term-memory store you
|
|
76
|
+
# pass as store= to graph.compile). Floor is the LangGraph 1.x line; BaseStore
|
|
77
|
+
# and its Op/Item/SearchItem types are re-exported from langgraph-checkpoint
|
|
78
|
+
# and stable across the 1.x series.
|
|
79
|
+
langgraph = [
|
|
80
|
+
"langgraph>=1.0.0",
|
|
81
|
+
]
|
|
82
|
+
# LlamaIndex integration — pip install clsplusplus[llamaindex]
|
|
83
|
+
# Provides clsplusplus.integrations.llamaindex.CLSMemoryBlock, a native subclass
|
|
84
|
+
# of llama_index.core.memory.BaseMemoryBlock (a pluggable memory_block you pass
|
|
85
|
+
# to Memory.from_defaults). Floor is the 0.12 line that introduced the modern
|
|
86
|
+
# Memory + BaseMemoryBlock API; verified against llama-index-core 0.14.x.
|
|
87
|
+
llamaindex = [
|
|
88
|
+
"llama-index-core>=0.12.0",
|
|
89
|
+
]
|
|
90
|
+
# AutoGen integration — pip install clsplusplus[autogen]
|
|
91
|
+
# Provides clsplusplus.integrations.autogen.CLSMemory, a native implementation
|
|
92
|
+
# of Microsoft AutoGen's autogen_core.memory.Memory protocol (the object you
|
|
93
|
+
# pass to AssistantAgent(memory=[...])). Floors target the new AutoGen line
|
|
94
|
+
# (autogen-core / autogen-agentchat 0.4+); the Memory protocol + MemoryContent /
|
|
95
|
+
# MemoryQueryResult / UpdateContextResult shapes are stable across 0.4 → 0.7.x.
|
|
96
|
+
autogen = [
|
|
97
|
+
"autogen-core>=0.4.0",
|
|
98
|
+
"autogen-agentchat>=0.4.0",
|
|
99
|
+
]
|
|
66
100
|
dev = [
|
|
67
101
|
"pytest>=7.4.0",
|
|
68
102
|
"pytest-asyncio>=0.23.0",
|
|
@@ -25,6 +25,7 @@ from clsplusplus.user_service import UserService
|
|
|
25
25
|
from clsplusplus.middleware import AuthMiddleware, HealthMetricsMiddleware, QuotaMiddleware, RateLimitMiddleware, RequestIdMiddleware, TracingMiddleware
|
|
26
26
|
from clsplusplus.abuse_guard import AbuseGuardMiddleware
|
|
27
27
|
from clsplusplus.tracer import tracer
|
|
28
|
+
from clsplusplus import write_metrics
|
|
28
29
|
from clsplusplus.models import (
|
|
29
30
|
AdjudicateRequest,
|
|
30
31
|
ApiKeyCreate,
|
|
@@ -1232,7 +1233,14 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
|
|
|
1232
1233
|
"store": "Setup",
|
|
1233
1234
|
"error": "Add CLS_REDIS_URL and CLS_DATABASE_URL in Render Dashboard.",
|
|
1234
1235
|
}
|
|
1235
|
-
|
|
1236
|
+
wm = write_metrics.snapshot()
|
|
1237
|
+
return HealthResponse(
|
|
1238
|
+
status=h["status"],
|
|
1239
|
+
stores=stores,
|
|
1240
|
+
writes=wm["total_writes"],
|
|
1241
|
+
degraded_writes=wm["degraded_writes"],
|
|
1242
|
+
degraded_write_rate=wm["degraded_write_rate"],
|
|
1243
|
+
)
|
|
1236
1244
|
|
|
1237
1245
|
@app.get("/v1/health/score", response_model=HealthResponse)
|
|
1238
1246
|
async def health_score():
|
|
@@ -2430,6 +2438,241 @@ def create_app(settings: Optional[Settings] = None) -> FastAPI:
|
|
|
2430
2438
|
logger.error("Admin user list error: %s: %s", type(e).__name__, e)
|
|
2431
2439
|
raise HTTPException(status_code=500, detail="User data unavailable")
|
|
2432
2440
|
|
|
2441
|
+
@app.get("/admin/integrations")
|
|
2442
|
+
async def admin_integrations(request: Request):
|
|
2443
|
+
"""Per-integration release sync: LIVE published version of each
|
|
2444
|
+
publishable artifact vs the current main release tag.
|
|
2445
|
+
|
|
2446
|
+
The "main release tag" is the single source of truth that every
|
|
2447
|
+
integration is supposed to track (the rule: the release tag IS the
|
|
2448
|
+
version for every artifact). We read it from the newest git tag; if
|
|
2449
|
+
the deployed image has stripped `.git`, we fall back to the committed
|
|
2450
|
+
package version. Each integration's `live_version` is read from its
|
|
2451
|
+
own registry/manifest using the same checks the sync tooling uses.
|
|
2452
|
+
|
|
2453
|
+
Fully resilient: every external/registry probe degrades to a
|
|
2454
|
+
`live_version` of None and `in_sync: false` rather than failing the
|
|
2455
|
+
request. This endpoint NEVER returns 500.
|
|
2456
|
+
"""
|
|
2457
|
+
_require_admin(request)
|
|
2458
|
+
|
|
2459
|
+
# --- main release tag: the source of truth every artifact tracks ---
|
|
2460
|
+
def _read_main_tag() -> str:
|
|
2461
|
+
# 1) newest git tag (release-of-record).
|
|
2462
|
+
try:
|
|
2463
|
+
import subprocess
|
|
2464
|
+
repo_root = FilePath(__file__).resolve().parent.parent.parent
|
|
2465
|
+
out = subprocess.run(
|
|
2466
|
+
["git", "describe", "--tags", "--abbrev=0"],
|
|
2467
|
+
cwd=str(repo_root),
|
|
2468
|
+
capture_output=True,
|
|
2469
|
+
timeout=5,
|
|
2470
|
+
)
|
|
2471
|
+
if out.returncode == 0:
|
|
2472
|
+
tag = out.stdout.decode().strip()
|
|
2473
|
+
if tag:
|
|
2474
|
+
return tag
|
|
2475
|
+
except Exception:
|
|
2476
|
+
pass
|
|
2477
|
+
# 2) committed package version (prod images often strip .git).
|
|
2478
|
+
try:
|
|
2479
|
+
pyproject = FilePath(__file__).resolve().parent.parent.parent / "pyproject.toml"
|
|
2480
|
+
if pyproject.exists():
|
|
2481
|
+
for line in pyproject.read_text().splitlines():
|
|
2482
|
+
s = line.strip()
|
|
2483
|
+
if s.startswith("version") and "=" in s:
|
|
2484
|
+
v = s.split("=", 1)[1].strip().strip('"').strip("'")
|
|
2485
|
+
if v:
|
|
2486
|
+
return f"v{v}"
|
|
2487
|
+
except Exception:
|
|
2488
|
+
pass
|
|
2489
|
+
# 3) last resort: in-package __version__.
|
|
2490
|
+
try:
|
|
2491
|
+
from clsplusplus import __version__ as _v
|
|
2492
|
+
if _v:
|
|
2493
|
+
return f"v{_v}"
|
|
2494
|
+
except Exception:
|
|
2495
|
+
pass
|
|
2496
|
+
return "unknown"
|
|
2497
|
+
|
|
2498
|
+
def _norm(v):
|
|
2499
|
+
"""Normalise a version for comparison: drop leading 'v', None-safe."""
|
|
2500
|
+
if not v:
|
|
2501
|
+
return None
|
|
2502
|
+
return str(v).strip().lstrip("v") or None
|
|
2503
|
+
|
|
2504
|
+
def _http_json(url: str, timeout: float = 4.0):
|
|
2505
|
+
"""Best-effort HTTP GET -> parsed JSON; None on any failure."""
|
|
2506
|
+
try:
|
|
2507
|
+
import json as _json
|
|
2508
|
+
import urllib.request
|
|
2509
|
+
req = urllib.request.Request(url, headers={"User-Agent": "cls-admin-integrations"})
|
|
2510
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
2511
|
+
return _json.loads(resp.read().decode())
|
|
2512
|
+
except Exception:
|
|
2513
|
+
return None
|
|
2514
|
+
|
|
2515
|
+
# --- per-integration LIVE version probes (each isolated/best-effort) ---
|
|
2516
|
+
def _npm_live() -> Optional[str]:
|
|
2517
|
+
# registry.npmjs.org dist-tag latest (matches `npm view ... version`)
|
|
2518
|
+
data = _http_json("https://registry.npmjs.org/clsplusplus")
|
|
2519
|
+
if isinstance(data, dict):
|
|
2520
|
+
latest = (data.get("dist-tags") or {}).get("latest")
|
|
2521
|
+
if latest:
|
|
2522
|
+
return latest
|
|
2523
|
+
return None
|
|
2524
|
+
|
|
2525
|
+
def _pypi_live() -> Optional[str]:
|
|
2526
|
+
data = _http_json("https://pypi.org/pypi/clsplusplus/json")
|
|
2527
|
+
if isinstance(data, dict):
|
|
2528
|
+
return (data.get("info") or {}).get("version")
|
|
2529
|
+
return None
|
|
2530
|
+
|
|
2531
|
+
def _mcp_registry_live() -> Optional[str]:
|
|
2532
|
+
data = _http_json(
|
|
2533
|
+
"https://registry.modelcontextprotocol.io/v0/servers"
|
|
2534
|
+
"?search=io.github.rajamohan1950/cls-memory"
|
|
2535
|
+
)
|
|
2536
|
+
if isinstance(data, dict):
|
|
2537
|
+
servers = data.get("servers") or data.get("data") or []
|
|
2538
|
+
for s in servers if isinstance(servers, list) else []:
|
|
2539
|
+
name = s.get("name") if isinstance(s, dict) else None
|
|
2540
|
+
if name and "cls-memory" in name:
|
|
2541
|
+
# version may live at top level or nested under meta.
|
|
2542
|
+
return (
|
|
2543
|
+
s.get("version")
|
|
2544
|
+
or (s.get("version_detail") or {}).get("version")
|
|
2545
|
+
or (s.get("_meta") or {}).get("version")
|
|
2546
|
+
)
|
|
2547
|
+
return None
|
|
2548
|
+
|
|
2549
|
+
def _extension_live() -> Optional[str]:
|
|
2550
|
+
# The Chrome Web Store artifact == the committed manifest version.
|
|
2551
|
+
try:
|
|
2552
|
+
import json as _json
|
|
2553
|
+
manifest = FilePath(__file__).resolve().parent.parent.parent / "extension" / "manifest.json"
|
|
2554
|
+
if manifest.exists():
|
|
2555
|
+
return _json.loads(manifest.read_text()).get("version")
|
|
2556
|
+
except Exception:
|
|
2557
|
+
pass
|
|
2558
|
+
return None
|
|
2559
|
+
|
|
2560
|
+
def _remote_mcp_live() -> Optional[str]:
|
|
2561
|
+
# Live hosted MCP endpoint: serverInfo.version from initialize, if
|
|
2562
|
+
# exposed; the metadata doc otherwise. Best-effort only.
|
|
2563
|
+
doc = _http_json(
|
|
2564
|
+
"https://www.clsplusplus.com/.well-known/oauth-protected-resource",
|
|
2565
|
+
timeout=3.0,
|
|
2566
|
+
)
|
|
2567
|
+
if isinstance(doc, dict):
|
|
2568
|
+
v = doc.get("version") or (doc.get("serverInfo") or {}).get("version")
|
|
2569
|
+
if v:
|
|
2570
|
+
return v
|
|
2571
|
+
return None
|
|
2572
|
+
|
|
2573
|
+
try:
|
|
2574
|
+
# The version probes below each block on network I/O (urllib) or a
|
|
2575
|
+
# `git` subprocess. Offload every blocking probe to a worker thread
|
|
2576
|
+
# and run them concurrently so this async handler never stalls the
|
|
2577
|
+
# event loop (previously up to ~15s of serial blocking). Each probe
|
|
2578
|
+
# is already isolated/best-effort, so any failure degrades to None.
|
|
2579
|
+
(
|
|
2580
|
+
main_tag,
|
|
2581
|
+
npm_live,
|
|
2582
|
+
pypi_live,
|
|
2583
|
+
mcp_registry_live,
|
|
2584
|
+
extension_live,
|
|
2585
|
+
remote_mcp_live,
|
|
2586
|
+
) = await asyncio.gather(
|
|
2587
|
+
asyncio.to_thread(_read_main_tag),
|
|
2588
|
+
asyncio.to_thread(_npm_live),
|
|
2589
|
+
asyncio.to_thread(_pypi_live),
|
|
2590
|
+
asyncio.to_thread(_mcp_registry_live),
|
|
2591
|
+
asyncio.to_thread(_extension_live),
|
|
2592
|
+
asyncio.to_thread(_remote_mcp_live),
|
|
2593
|
+
)
|
|
2594
|
+
rel = _norm(main_tag)
|
|
2595
|
+
|
|
2596
|
+
# Aggregate usage (best-effort; shared across artifacts).
|
|
2597
|
+
usage = None
|
|
2598
|
+
try:
|
|
2599
|
+
usage = await _metrics.get_aggregate_metrics()
|
|
2600
|
+
except Exception:
|
|
2601
|
+
usage = None
|
|
2602
|
+
|
|
2603
|
+
# Static catalogue of publishable integrations + how to read each
|
|
2604
|
+
# one's LIVE version and where it is published.
|
|
2605
|
+
catalogue = [
|
|
2606
|
+
{
|
|
2607
|
+
"name": "npm (clsplusplus JS/TS SDK)",
|
|
2608
|
+
"live_version": npm_live,
|
|
2609
|
+
"publish_url": "https://www.npmjs.com/package/clsplusplus",
|
|
2610
|
+
"tracks_main_tag": True,
|
|
2611
|
+
},
|
|
2612
|
+
{
|
|
2613
|
+
"name": "PyPI (clsplusplus Python SDK + server + MCP)",
|
|
2614
|
+
"live_version": pypi_live,
|
|
2615
|
+
"publish_url": "https://pypi.org/project/clsplusplus/",
|
|
2616
|
+
"tracks_main_tag": True,
|
|
2617
|
+
},
|
|
2618
|
+
{
|
|
2619
|
+
"name": "MCP Registry (io.github.rajamohan1950/cls-memory)",
|
|
2620
|
+
"live_version": mcp_registry_live,
|
|
2621
|
+
"publish_url": (
|
|
2622
|
+
"https://registry.modelcontextprotocol.io/v0/servers"
|
|
2623
|
+
"?search=io.github.rajamohan1950/cls-memory"
|
|
2624
|
+
),
|
|
2625
|
+
"tracks_main_tag": True,
|
|
2626
|
+
},
|
|
2627
|
+
{
|
|
2628
|
+
"name": "Chrome Web Store (CLS++ Memory extension)",
|
|
2629
|
+
"live_version": extension_live,
|
|
2630
|
+
"publish_url": "https://chromewebstore.google.com/",
|
|
2631
|
+
# Extension is intentionally on its own 6.x track.
|
|
2632
|
+
"tracks_main_tag": False,
|
|
2633
|
+
},
|
|
2634
|
+
{
|
|
2635
|
+
"name": "Remote MCP server (clsplusplus.com/mcp)",
|
|
2636
|
+
"live_version": remote_mcp_live,
|
|
2637
|
+
"publish_url": "https://www.clsplusplus.com/mcp",
|
|
2638
|
+
# A running service tracks the deployed release.
|
|
2639
|
+
"tracks_main_tag": True,
|
|
2640
|
+
},
|
|
2641
|
+
]
|
|
2642
|
+
|
|
2643
|
+
integrations = []
|
|
2644
|
+
for entry in catalogue:
|
|
2645
|
+
live = entry["live_version"]
|
|
2646
|
+
if entry["tracks_main_tag"]:
|
|
2647
|
+
in_sync = bool(live) and rel is not None and _norm(live) == rel
|
|
2648
|
+
else:
|
|
2649
|
+
# Decoupled track: "in sync" only means we could read it.
|
|
2650
|
+
in_sync = bool(live)
|
|
2651
|
+
integrations.append({
|
|
2652
|
+
"name": entry["name"],
|
|
2653
|
+
"live_version": live,
|
|
2654
|
+
"main_tag": main_tag,
|
|
2655
|
+
"in_sync": in_sync,
|
|
2656
|
+
"publish_url": entry["publish_url"],
|
|
2657
|
+
"usage": usage,
|
|
2658
|
+
})
|
|
2659
|
+
|
|
2660
|
+
return {"main_tag": main_tag, "integrations": integrations}
|
|
2661
|
+
except Exception as e:
|
|
2662
|
+
# Defensive: this endpoint must never 500. Degrade to a fully
|
|
2663
|
+
# "unknown" report so the dashboard always renders.
|
|
2664
|
+
logger.error("Admin integrations error: %s: %s", type(e).__name__, e)
|
|
2665
|
+
tag = "unknown"
|
|
2666
|
+
try:
|
|
2667
|
+
tag = _read_main_tag()
|
|
2668
|
+
except Exception:
|
|
2669
|
+
pass
|
|
2670
|
+
return {
|
|
2671
|
+
"main_tag": tag,
|
|
2672
|
+
"integrations": [],
|
|
2673
|
+
"error": "integration data degraded",
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2433
2676
|
def _require_admin(request: Request):
|
|
2434
2677
|
"""Helper to enforce admin access on endpoints."""
|
|
2435
2678
|
if not getattr(request.state, "is_admin", False):
|
|
@@ -114,6 +114,14 @@ class Settings(BaseSettings):
|
|
|
114
114
|
# Embeddings (used by L1/L2/L3 tiers, not by phase engine)
|
|
115
115
|
embedding_model: str = "all-MiniLM-L6-v2"
|
|
116
116
|
embedding_dim: int = 384
|
|
117
|
+
# Embedding backend. "sentence-transformers" (default) keeps the local
|
|
118
|
+
# all-MiniLM-L6-v2 behavior unchanged. Set CLS_EMBEDDING_PROVIDER=openai to
|
|
119
|
+
# use the OpenAI embeddings API instead — openai is lazy-imported and is NOT
|
|
120
|
+
# a hard dependency; if it's missing the provider falls back to
|
|
121
|
+
# sentence-transformers. openai_api_key is read from the existing setting.
|
|
122
|
+
embedding_provider: str = "sentence-transformers" # CLS_EMBEDDING_PROVIDER
|
|
123
|
+
openai_embedding_model: str = "text-embedding-3-small" # CLS_OPENAI_EMBEDDING_MODEL
|
|
124
|
+
openai_embedding_dim: int = 1536 # CLS_OPENAI_EMBEDDING_DIM
|
|
117
125
|
|
|
118
126
|
# Plasticity coefficients (α, β, γ, λ, δ)
|
|
119
127
|
alpha_salience: float = 1.0
|
|
@@ -136,6 +144,11 @@ class Settings(BaseSettings):
|
|
|
136
144
|
similarity_threshold: float = 0.7
|
|
137
145
|
conflict_threshold: float = 0.3
|
|
138
146
|
quorum_threshold: float = 0.8
|
|
147
|
+
# Belief-revision-on-write: when ON, the normal write() path routes new
|
|
148
|
+
# facts through the ReconsolidationGate to detect contradictions against
|
|
149
|
+
# existing same-namespace items before storing. OFF by default so the
|
|
150
|
+
# current write behavior is unchanged.
|
|
151
|
+
belief_revision_on_write: bool = False # CLS_BELIEF_REVISION_ON_WRITE
|
|
139
152
|
|
|
140
153
|
# Working buffer
|
|
141
154
|
l0_capacity_tokens: int = 4096
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Embedding service for CLS++."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from clsplusplus.config import Settings
|
|
9
|
+
from clsplusplus.models import MemoryItem
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseEmbeddingProvider:
|
|
15
|
+
"""Interface for embedding backends.
|
|
16
|
+
|
|
17
|
+
A provider turns text into dense vectors. Implementations are lazy: the
|
|
18
|
+
underlying model/client is loaded on first use, never at construction.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name: str = "base"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def dim(self) -> int:
|
|
25
|
+
"""Dimensionality of the vectors this provider returns."""
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
def embed(self, texts: list[str]) -> list[list[float]]:
|
|
29
|
+
"""Embed a batch of texts. Returns one vector per input text.
|
|
30
|
+
|
|
31
|
+
On failure (model/client unavailable) returns empty vectors so callers
|
|
32
|
+
never break — same contract the service has always honored.
|
|
33
|
+
"""
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SentenceTransformerProvider(BaseEmbeddingProvider):
|
|
38
|
+
"""Local sentence-transformers backend (default, unchanged behavior).
|
|
39
|
+
|
|
40
|
+
Wraps the original all-MiniLM-L6-v2 logic: lazy model load, returns empty
|
|
41
|
+
vectors when sentence-transformers is not installed.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
name = "sentence-transformers"
|
|
45
|
+
|
|
46
|
+
def __init__(self, settings: Settings):
|
|
47
|
+
self.settings = settings
|
|
48
|
+
self._model = None
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def model(self):
|
|
52
|
+
"""Lazy load the embedding model."""
|
|
53
|
+
if self._model is None:
|
|
54
|
+
try:
|
|
55
|
+
from sentence_transformers import SentenceTransformer
|
|
56
|
+
self._model = SentenceTransformer(self.settings.embedding_model)
|
|
57
|
+
except ImportError:
|
|
58
|
+
self._model = False # sentinel: unavailable
|
|
59
|
+
return self._model
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def dim(self) -> int:
|
|
63
|
+
return self.settings.embedding_dim
|
|
64
|
+
|
|
65
|
+
def embed(self, texts: list[str]) -> list[list[float]]:
|
|
66
|
+
if self.model is False:
|
|
67
|
+
return [[] for _ in texts]
|
|
68
|
+
return self.model.encode(texts, convert_to_numpy=True).tolist()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class OpenAIProvider(BaseEmbeddingProvider):
|
|
72
|
+
"""OpenAI embeddings backend.
|
|
73
|
+
|
|
74
|
+
Selected via CLS_EMBEDDING_PROVIDER=openai. openai is lazy-imported and is
|
|
75
|
+
NOT a hard dependency. If the client/key is unavailable the embed call
|
|
76
|
+
returns empty vectors; selection-time fallback to sentence-transformers is
|
|
77
|
+
handled by the service factory.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
name = "openai"
|
|
81
|
+
|
|
82
|
+
def __init__(self, settings: Settings):
|
|
83
|
+
self.settings = settings
|
|
84
|
+
self._client = None
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def client(self):
|
|
88
|
+
"""Lazy build the OpenAI client."""
|
|
89
|
+
if self._client is None:
|
|
90
|
+
try:
|
|
91
|
+
from openai import OpenAI
|
|
92
|
+
self._client = OpenAI(api_key=self.settings.openai_api_key)
|
|
93
|
+
except ImportError:
|
|
94
|
+
self._client = False # sentinel: unavailable
|
|
95
|
+
return self._client
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def dim(self) -> int:
|
|
99
|
+
return self.settings.openai_embedding_dim
|
|
100
|
+
|
|
101
|
+
def embed(self, texts: list[str]) -> list[list[float]]:
|
|
102
|
+
if self.client is False:
|
|
103
|
+
return [[] for _ in texts]
|
|
104
|
+
try:
|
|
105
|
+
resp = self.client.embeddings.create(
|
|
106
|
+
model=self.settings.openai_embedding_model,
|
|
107
|
+
input=texts,
|
|
108
|
+
)
|
|
109
|
+
return [d.embedding for d in resp.data]
|
|
110
|
+
except Exception as e: # network / auth / quota — never break the caller
|
|
111
|
+
logger.warning("OpenAI embedding failed (returning empty vectors): %s", e)
|
|
112
|
+
return [[] for _ in texts]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _build_provider(settings: Settings) -> BaseEmbeddingProvider:
|
|
116
|
+
"""Select the embedding provider from settings.
|
|
117
|
+
|
|
118
|
+
Default is sentence-transformers. CLS_EMBEDDING_PROVIDER=openai selects
|
|
119
|
+
OpenAI, but if the openai package is not installed we warn and fall back to
|
|
120
|
+
sentence-transformers so behavior degrades gracefully.
|
|
121
|
+
"""
|
|
122
|
+
provider = (settings.embedding_provider or "").strip().lower()
|
|
123
|
+
if provider == "openai":
|
|
124
|
+
try:
|
|
125
|
+
import openai # noqa: F401
|
|
126
|
+
except ImportError:
|
|
127
|
+
logger.warning(
|
|
128
|
+
"CLS_EMBEDDING_PROVIDER=openai but the openai package is not "
|
|
129
|
+
"installed; falling back to sentence-transformers."
|
|
130
|
+
)
|
|
131
|
+
return SentenceTransformerProvider(settings)
|
|
132
|
+
return OpenAIProvider(settings)
|
|
133
|
+
return SentenceTransformerProvider(settings)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class EmbeddingService:
|
|
137
|
+
"""Produces embeddings for memory items."""
|
|
138
|
+
|
|
139
|
+
def __init__(self, settings: Optional[Settings] = None):
|
|
140
|
+
self.settings = settings or Settings()
|
|
141
|
+
self.provider = _build_provider(self.settings)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def model(self):
|
|
145
|
+
"""Backward-compat accessor for the underlying sentence-transformers model.
|
|
146
|
+
|
|
147
|
+
Returns None for non-sentence-transformers providers.
|
|
148
|
+
"""
|
|
149
|
+
return getattr(self.provider, "model", None)
|
|
150
|
+
|
|
151
|
+
def embed(self, text: str) -> list[float]:
|
|
152
|
+
"""Embed a single text. Returns empty list if provider unavailable."""
|
|
153
|
+
out = self.provider.embed([text])
|
|
154
|
+
return out[0] if out else []
|
|
155
|
+
|
|
156
|
+
def embed_batch(self, texts: list[str]) -> list[list[float]]:
|
|
157
|
+
"""Embed multiple texts. Returns empty lists if provider unavailable."""
|
|
158
|
+
if not texts:
|
|
159
|
+
return []
|
|
160
|
+
return self.provider.embed(texts)
|
|
161
|
+
|
|
162
|
+
def embed_item(self, item: MemoryItem) -> MemoryItem:
|
|
163
|
+
"""Add embedding to a memory item."""
|
|
164
|
+
if not item.embedding:
|
|
165
|
+
item.embedding = self.embed(item.text)
|
|
166
|
+
return item
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def cosine_similarity(a: list[float], b: list[float]) -> float:
|
|
170
|
+
"""Compute cosine similarity between two vectors."""
|
|
171
|
+
va = np.array(a)
|
|
172
|
+
vb = np.array(b)
|
|
173
|
+
return float(np.dot(va, vb) / (np.linalg.norm(va) * np.linalg.norm(vb) + 1e-9))
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Third-party framework integrations for CLS++.
|
|
2
|
+
|
|
3
|
+
Each submodule adapts the CLS++ memory layer to a specific agent framework so
|
|
4
|
+
that framework users can plug CLS++ in as their persistent memory backend
|
|
5
|
+
without touching the HTTP client directly.
|
|
6
|
+
|
|
7
|
+
Currently provided:
|
|
8
|
+
|
|
9
|
+
clsplusplus.integrations.crewai.CLSMemoryStorage
|
|
10
|
+
Implements CrewAI's ``crewai.memory.storage.interface.Storage``
|
|
11
|
+
contract (``save`` / ``search`` / ``reset``), delegating to the CLS++
|
|
12
|
+
``Brain`` client. Wire it into a Crew via ``external_memory=``.
|
|
13
|
+
|
|
14
|
+
clsplusplus.integrations.langgraph.CLSMemoryStore
|
|
15
|
+
Subclasses LangGraph's ``langgraph.store.base.BaseStore`` (the
|
|
16
|
+
long-term-memory ``store=`` you pass to ``graph.compile``), delegating
|
|
17
|
+
to the CLS++ ``Brain`` client. Requires ``pip install
|
|
18
|
+
clsplusplus[langgraph]``.
|
|
19
|
+
|
|
20
|
+
Integrations keep their framework imports lazy: importing ``clsplusplus`` (or
|
|
21
|
+
``clsplusplus.integrations``) never requires the framework to be installed.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
__all__ = ["CLSMemoryStorage", "CLSMemoryStore", "CLSMemoryBlock", "CLSMemory"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def __getattr__(name: str):
|
|
30
|
+
if name == "CLSMemoryStorage":
|
|
31
|
+
from clsplusplus.integrations.crewai import CLSMemoryStorage
|
|
32
|
+
|
|
33
|
+
return CLSMemoryStorage
|
|
34
|
+
if name == "CLSMemoryStore":
|
|
35
|
+
from clsplusplus.integrations.langgraph import CLSMemoryStore
|
|
36
|
+
|
|
37
|
+
return CLSMemoryStore
|
|
38
|
+
if name == "CLSMemoryBlock":
|
|
39
|
+
from clsplusplus.integrations.llamaindex import CLSMemoryBlock
|
|
40
|
+
|
|
41
|
+
return CLSMemoryBlock
|
|
42
|
+
if name == "CLSMemory":
|
|
43
|
+
from clsplusplus.integrations.autogen import CLSMemory
|
|
44
|
+
|
|
45
|
+
return CLSMemory
|
|
46
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|