cite-agent 1.3.6__py3-none-any.whl → 1.3.7__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.
Potentially problematic release.
This version of cite-agent might be problematic. Click here for more details.
- cite_agent/__version__.py +1 -1
- cite_agent/cli.py +9 -2
- cite_agent/enhanced_ai_agent.py +332 -73
- {cite_agent-1.3.6.dist-info → cite_agent-1.3.7.dist-info}/METADATA +1 -1
- cite_agent-1.3.7.dist-info/RECORD +31 -0
- {cite_agent-1.3.6.dist-info → cite_agent-1.3.7.dist-info}/top_level.txt +0 -1
- cite_agent-1.3.6.dist-info/RECORD +0 -57
- src/__init__.py +0 -1
- src/services/__init__.py +0 -132
- src/services/auth_service/__init__.py +0 -3
- src/services/auth_service/auth_manager.py +0 -33
- src/services/graph/__init__.py +0 -1
- src/services/graph/knowledge_graph.py +0 -194
- src/services/llm_service/__init__.py +0 -5
- src/services/llm_service/llm_manager.py +0 -495
- src/services/paper_service/__init__.py +0 -5
- src/services/paper_service/openalex.py +0 -231
- src/services/performance_service/__init__.py +0 -1
- src/services/performance_service/rust_performance.py +0 -395
- src/services/research_service/__init__.py +0 -23
- src/services/research_service/chatbot.py +0 -2056
- src/services/research_service/citation_manager.py +0 -436
- src/services/research_service/context_manager.py +0 -1441
- src/services/research_service/conversation_manager.py +0 -597
- src/services/research_service/critical_paper_detector.py +0 -577
- src/services/research_service/enhanced_research.py +0 -121
- src/services/research_service/enhanced_synthesizer.py +0 -375
- src/services/research_service/query_generator.py +0 -777
- src/services/research_service/synthesizer.py +0 -1273
- src/services/search_service/__init__.py +0 -5
- src/services/search_service/indexer.py +0 -186
- src/services/search_service/search_engine.py +0 -342
- src/services/simple_enhanced_main.py +0 -287
- {cite_agent-1.3.6.dist-info → cite_agent-1.3.7.dist-info}/WHEEL +0 -0
- {cite_agent-1.3.6.dist-info → cite_agent-1.3.7.dist-info}/entry_points.txt +0 -0
- {cite_agent-1.3.6.dist-info → cite_agent-1.3.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
cite_agent/__init__.py,sha256=wAXV2v8nNOmIAd0rh8196ItBl9hHWBVOBl5Re4VB77I,1645
|
|
2
|
-
cite_agent/__main__.py,sha256=6x3lltwG-iZHeQbN12rwvdkPDfd2Rmdk71tOOaC89Mw,179
|
|
3
|
-
cite_agent/__version__.py,sha256=5ZbAQtod5QalTI1C2N07edlxplzG_Q2XvGOSyOok4uA,22
|
|
4
|
-
cite_agent/account_client.py,sha256=yLuzhIJoIZuXHXGbaVMzDxRATQwcy-wiaLnUrDuwUhI,5725
|
|
5
|
-
cite_agent/agent_backend_only.py,sha256=H4DH4hmKhT0T3rQLAb2xnnJVjxl3pOZaljL9r6JndFY,6314
|
|
6
|
-
cite_agent/ascii_plotting.py,sha256=lk8BaECs6fmjtp4iH12G09-frlRehAN7HLhHt2crers,8570
|
|
7
|
-
cite_agent/auth.py,sha256=YtoGXKwcLkZQbop37iYYL9BzRWBRPlt_D9p71VGViS4,9833
|
|
8
|
-
cite_agent/backend_only_client.py,sha256=WqLF8x7aXTro2Q3ehqKMsdCg53s6fNk9Hy86bGxqmmw,2561
|
|
9
|
-
cite_agent/cli.py,sha256=Qq0Gt7sNVR4R2ue10KFZElOPYrujBG9xTOqy2qetxL4,35562
|
|
10
|
-
cite_agent/cli_conversational.py,sha256=RAmgRNRyB8gQ8QLvWU-Tt23j2lmA34rQNT5F3_7SOq0,11141
|
|
11
|
-
cite_agent/cli_enhanced.py,sha256=EAaSw9qtiYRWUXF6_05T19GCXlz9cCSz6n41ASnXIPc,7407
|
|
12
|
-
cite_agent/cli_workflow.py,sha256=4oS_jW9D8ylovXbEFdsyLQONt4o0xxR4Xatfcc4tnBs,11641
|
|
13
|
-
cite_agent/dashboard.py,sha256=VGV5XQU1PnqvTsxfKMcue3j2ri_nvm9Be6O5aVays_w,10502
|
|
14
|
-
cite_agent/enhanced_ai_agent.py,sha256=hOL17pDKQdD1MJZRXkjEBlqyNmTdA_pcxIkQEojysFM,172282
|
|
15
|
-
cite_agent/project_detector.py,sha256=fPl5cLTy_oyufqrQ7RJ5IRVdofZoPqDRaQXW6tRtBJc,6086
|
|
16
|
-
cite_agent/rate_limiter.py,sha256=-0fXx8Tl4zVB4O28n9ojU2weRo-FBF1cJo9Z5jC2LxQ,10908
|
|
17
|
-
cite_agent/session_manager.py,sha256=B0MXSOsXdhO3DlvTG7S8x6pmGlYEDvIZ-o8TZM23niQ,9444
|
|
18
|
-
cite_agent/setup_config.py,sha256=3m2e3gw0srEWA0OygdRo64r-8HK5ohyXfct0c__CF3s,16817
|
|
19
|
-
cite_agent/streaming_ui.py,sha256=N6TWOo7GVQ_Ynfw73JCfrdGcLIU-PwbS3GbsHQHegmg,7810
|
|
20
|
-
cite_agent/telemetry.py,sha256=55kXdHvI24ZsEkbFtihcjIfJt2oiSXcEpLzTxQ3KCdQ,2916
|
|
21
|
-
cite_agent/ui.py,sha256=r1OAeY3NSeqhAjJYmEBH9CaennBuibFAz1Mur6YF80E,6134
|
|
22
|
-
cite_agent/updater.py,sha256=udoAAN4gBKAvKDV7JTh2FJO_jIhNk9bby4x6n188MEY,8458
|
|
23
|
-
cite_agent/web_search.py,sha256=FZCuNO7MAITiOIbpPbJyt2bzbXPzQla-9amJpnMpW_4,6520
|
|
24
|
-
cite_agent/workflow.py,sha256=a0YC0Mzz4or1C5t2gZcuJBQ0uMOZrooaI8eLu2kkI0k,15086
|
|
25
|
-
cite_agent/workflow_integration.py,sha256=A9ua0DN5pRtuU0cAwrUTGvqt2SXKhEHQbrHx16EGnDM,10910
|
|
26
|
-
cite_agent-1.3.6.dist-info/licenses/LICENSE,sha256=XJkyO4IymhSUniN1ENY6lLrL2729gn_rbRlFK6_Hi9M,1074
|
|
27
|
-
src/__init__.py,sha256=0eEpjRfjRjOTilP66y-AbGNslBsVYr_clE-bZUzsX7s,40
|
|
28
|
-
src/services/__init__.py,sha256=pTGLCH_84mz4nGtYMwQES5w-LzoSulUtx_uuNM6r-LA,4257
|
|
29
|
-
src/services/simple_enhanced_main.py,sha256=IJoOplCqcVUg3GvN_BRyAhpGrLm_WEPy2jmHcNCY6R0,9257
|
|
30
|
-
src/services/auth_service/__init__.py,sha256=VVFfBUr_GMJuxVH_553D2PZmZ9vhHeab9_qiJEf-g6Q,38
|
|
31
|
-
src/services/auth_service/auth_manager.py,sha256=MJdWFE36R_htoyBbjgGSTSx2Py61sTM3lhBjXBZ4Bog,873
|
|
32
|
-
src/services/graph/__init__.py,sha256=jheRQ-x652RZ68fKyUqUNGXmTAJsp5URVMhlOauFRO0,29
|
|
33
|
-
src/services/graph/knowledge_graph.py,sha256=ips2IpVpxDFkdPku4XKgZNRnoR2NjZqZk3xbIArJaaM,7348
|
|
34
|
-
src/services/llm_service/__init__.py,sha256=eNAsQpJtVXpJENb-gHtpKzWpncnHHAMB05EI48wrugQ,122
|
|
35
|
-
src/services/llm_service/llm_manager.py,sha256=6o5KN-3wJ0hT8PS9hPMpTGS6G9SlleSzYsXZQRjj_vI,21027
|
|
36
|
-
src/services/paper_service/__init__.py,sha256=0ONhTf_3H81l5y6EqHMRZd5dCXLAXDa-gbYwge84zKA,142
|
|
37
|
-
src/services/paper_service/openalex.py,sha256=pPhPcHMK2gQJCUVPB4ujE8xya0UqUvfcN95cy5ooP68,8801
|
|
38
|
-
src/services/performance_service/__init__.py,sha256=48bYfW4pzf-FG9644kTnNwGyD1tJJ7tVn3cD3r_ZAbk,65
|
|
39
|
-
src/services/performance_service/rust_performance.py,sha256=n-FzJ98XslmpUAkmmuaunYDTPz-9ZY-qL4oWAoBAaoA,15558
|
|
40
|
-
src/services/research_service/__init__.py,sha256=ZCBzSUdstHqwMmJ1x0kJK4PkRlv9OrSOEFeQFoVM-7M,813
|
|
41
|
-
src/services/research_service/chatbot.py,sha256=12pVAoe_fd2RXi6_cP-fxfRnWyJStsyn8znVu5cy9qo,91153
|
|
42
|
-
src/services/research_service/citation_manager.py,sha256=vzyVivBS0_9IiFE-wOH9hiLiC-fpHmiaZpR1084DenE,16586
|
|
43
|
-
src/services/research_service/context_manager.py,sha256=FGbeylLWKvgoA5fElyiqg5IhnMBIZ-t3w0oDHN4Zy1E,61332
|
|
44
|
-
src/services/research_service/conversation_manager.py,sha256=-rdzURzu-SiqozyeQLid5a5lS-KzIqGDozdE8BG-DTs,22854
|
|
45
|
-
src/services/research_service/critical_paper_detector.py,sha256=gc3oZHB8RqDhxFqJx21NoKLcHmmqHXRo0eXY-AL5KSc,21941
|
|
46
|
-
src/services/research_service/enhanced_research.py,sha256=5B8zZjJ2iSLEgnjfyDKow5x_MLRANLJdMbLmmPR5Lc0,4268
|
|
47
|
-
src/services/research_service/enhanced_synthesizer.py,sha256=puJg2C10KXryCMPkec-chC4rxbIJdFFswo7w4rbaXkc,16603
|
|
48
|
-
src/services/research_service/query_generator.py,sha256=LcFTGsewE6l2LRgUI2E6fXAcpy4vaYaUFFfZhI_WlYU,30707
|
|
49
|
-
src/services/research_service/synthesizer.py,sha256=lCcu37PWhWVNphHKaJJDIC-JQ5OINAN7OJ7iV9BWAvM,52557
|
|
50
|
-
src/services/search_service/__init__.py,sha256=UZFXdd7r6wietQ2kESXEyGffdfBbpghquecQde7auF4,137
|
|
51
|
-
src/services/search_service/indexer.py,sha256=u3-uwdAfmahWWsdebDF9i8XIyp7YtUMIHzlmBLBnPPM,7252
|
|
52
|
-
src/services/search_service/search_engine.py,sha256=S9HqQ_mk-8W4d4MUOgBbEGQGV29-eSuceSFvVb4Xk-k,12500
|
|
53
|
-
cite_agent-1.3.6.dist-info/METADATA,sha256=Nw2biNkpNAmCACEe_2dJ5dGy-KvL7DtxfFuNhgNWIN4,12231
|
|
54
|
-
cite_agent-1.3.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
55
|
-
cite_agent-1.3.6.dist-info/entry_points.txt,sha256=bJ0u28nFIxQKH1PWQ2ak4PV-FAjhoxTC7YADEdDenFw,83
|
|
56
|
-
cite_agent-1.3.6.dist-info/top_level.txt,sha256=TgOFqJTIy8vDZuOoYA2QgagkqZtfhM5Acvt_IsWzAKo,15
|
|
57
|
-
cite_agent-1.3.6.dist-info/RECORD,,
|
src/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Project root package initializer."""
|
src/services/__init__.py
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Main services package for AI services layer
|
|
3
|
-
Provides unified access to all service components
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
# Import all major service classes for easy access
|
|
7
|
-
from .llm_service.llm_manager import LLMManager
|
|
8
|
-
from .research_service.enhanced_research import EnhancedResearchService
|
|
9
|
-
from .context_manager.advanced_context import AdvancedContextManager
|
|
10
|
-
from .tool_framework.tool_manager import ToolManager
|
|
11
|
-
from .auth_service.auth_manager import auth_manager
|
|
12
|
-
|
|
13
|
-
# Service registry for dependency injection
|
|
14
|
-
SERVICE_REGISTRY = {}
|
|
15
|
-
|
|
16
|
-
def register_service(name: str, service_instance):
|
|
17
|
-
"""Register a service instance in the global registry"""
|
|
18
|
-
SERVICE_REGISTRY[name] = service_instance
|
|
19
|
-
|
|
20
|
-
def get_service(name: str):
|
|
21
|
-
"""Get a service instance from the registry"""
|
|
22
|
-
return SERVICE_REGISTRY.get(name)
|
|
23
|
-
|
|
24
|
-
def initialize_services(config: dict = None):
|
|
25
|
-
"""Initialize all core services with configuration"""
|
|
26
|
-
config = config or {}
|
|
27
|
-
|
|
28
|
-
# Initialize services (with minimal config for testing)
|
|
29
|
-
services = {}
|
|
30
|
-
|
|
31
|
-
try:
|
|
32
|
-
# LLM Manager (needs redis_url but we'll handle gracefully)
|
|
33
|
-
redis_url = config.get('redis_url', 'redis://localhost:6379')
|
|
34
|
-
llm_manager = LLMManager(redis_url=redis_url)
|
|
35
|
-
services['llm_manager'] = llm_manager
|
|
36
|
-
register_service('llm_manager', llm_manager)
|
|
37
|
-
except Exception as e:
|
|
38
|
-
# Graceful fallback for testing
|
|
39
|
-
print(f"LLM Manager initialization skipped: {e}")
|
|
40
|
-
|
|
41
|
-
try:
|
|
42
|
-
# Research Service
|
|
43
|
-
research_service = EnhancedResearchService()
|
|
44
|
-
services['research_service'] = research_service
|
|
45
|
-
register_service('research_service', research_service)
|
|
46
|
-
except Exception as e:
|
|
47
|
-
print(f"Research Service initialization skipped: {e}")
|
|
48
|
-
|
|
49
|
-
try:
|
|
50
|
-
# Context Manager
|
|
51
|
-
context_manager = AdvancedContextManager()
|
|
52
|
-
services['context_manager'] = context_manager
|
|
53
|
-
register_service('context_manager', context_manager)
|
|
54
|
-
except Exception as e:
|
|
55
|
-
print(f"Context Manager initialization skipped: {e}")
|
|
56
|
-
|
|
57
|
-
try:
|
|
58
|
-
# Tool Manager
|
|
59
|
-
tool_manager = ToolManager()
|
|
60
|
-
services['tool_manager'] = tool_manager
|
|
61
|
-
register_service('tool_manager', tool_manager)
|
|
62
|
-
except Exception as e:
|
|
63
|
-
print(f"Tool Manager initialization skipped: {e}")
|
|
64
|
-
|
|
65
|
-
return services
|
|
66
|
-
|
|
67
|
-
class ServiceLayer:
|
|
68
|
-
"""Unified service layer for easy access to all AI services"""
|
|
69
|
-
|
|
70
|
-
def __init__(self, config: dict = None):
|
|
71
|
-
self.config = config or {}
|
|
72
|
-
self.services = {}
|
|
73
|
-
self._initialized = False
|
|
74
|
-
|
|
75
|
-
def initialize(self):
|
|
76
|
-
"""Initialize all services"""
|
|
77
|
-
if self._initialized:
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
self.services = initialize_services(self.config)
|
|
81
|
-
self._initialized = True
|
|
82
|
-
|
|
83
|
-
@property
|
|
84
|
-
def llm_manager(self) -> LLMManager:
|
|
85
|
-
"""Get LLM Manager service"""
|
|
86
|
-
return self.services.get('llm_manager')
|
|
87
|
-
|
|
88
|
-
@property
|
|
89
|
-
def research_service(self) -> EnhancedResearchService:
|
|
90
|
-
"""Get Research service"""
|
|
91
|
-
return self.services.get('research_service')
|
|
92
|
-
|
|
93
|
-
@property
|
|
94
|
-
def context_manager(self) -> AdvancedContextManager:
|
|
95
|
-
"""Get Context Manager service"""
|
|
96
|
-
return self.services.get('context_manager')
|
|
97
|
-
|
|
98
|
-
@property
|
|
99
|
-
def tool_manager(self) -> ToolManager:
|
|
100
|
-
"""Get Tool Manager service"""
|
|
101
|
-
return self.services.get('tool_manager')
|
|
102
|
-
|
|
103
|
-
def get_health_status(self) -> dict:
|
|
104
|
-
"""Get health status of all services"""
|
|
105
|
-
status = {
|
|
106
|
-
"services_initialized": self._initialized,
|
|
107
|
-
"total_services": len(self.services),
|
|
108
|
-
"available_services": list(self.services.keys())
|
|
109
|
-
}
|
|
110
|
-
return status
|
|
111
|
-
|
|
112
|
-
# Global service layer instance
|
|
113
|
-
_service_layer = None
|
|
114
|
-
|
|
115
|
-
def get_service_layer(config: dict = None) -> ServiceLayer:
|
|
116
|
-
"""Get the global service layer instance"""
|
|
117
|
-
global _service_layer
|
|
118
|
-
if _service_layer is None:
|
|
119
|
-
_service_layer = ServiceLayer(config)
|
|
120
|
-
return _service_layer
|
|
121
|
-
|
|
122
|
-
# Export key classes and functions
|
|
123
|
-
__all__ = [
|
|
124
|
-
'LLMManager',
|
|
125
|
-
'EnhancedResearchService',
|
|
126
|
-
'AdvancedContextManager',
|
|
127
|
-
'ToolManager',
|
|
128
|
-
'ServiceLayer',
|
|
129
|
-
'get_service_layer',
|
|
130
|
-
'initialize_services',
|
|
131
|
-
'auth_manager'
|
|
132
|
-
]
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Basic authentication manager for testing
|
|
3
|
-
"""
|
|
4
|
-
from typing import Dict, Any, Optional
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class AuthManager:
|
|
8
|
-
"""Basic auth manager for testing purposes"""
|
|
9
|
-
|
|
10
|
-
def __init__(self):
|
|
11
|
-
self.test_user = {
|
|
12
|
-
"id": "test_user_123",
|
|
13
|
-
"username": "test_user",
|
|
14
|
-
"email": "test@example.com"
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async def get_current_user(self) -> Dict[str, Any]:
|
|
18
|
-
"""Return test user for testing"""
|
|
19
|
-
return self.test_user
|
|
20
|
-
|
|
21
|
-
def verify_token(self, token: str) -> Optional[Dict[str, Any]]:
|
|
22
|
-
"""Verify token (test implementation)"""
|
|
23
|
-
if token == "test_token":
|
|
24
|
-
return self.test_user
|
|
25
|
-
return None
|
|
26
|
-
|
|
27
|
-
def create_token(self, user_data: Dict[str, Any]) -> str:
|
|
28
|
-
"""Create token (test implementation)"""
|
|
29
|
-
return "test_token"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# Global instance
|
|
33
|
-
auth_manager = AuthManager()
|
src/services/graph/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Graph service package."""
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
"""Lightweight async knowledge graph implementation used by the research synthesizer.
|
|
2
|
-
|
|
3
|
-
The production design originally assumed an external graph database, but the launch-ready
|
|
4
|
-
runtime needs a dependable in-process implementation that works without external services.
|
|
5
|
-
This module provides a minimal yet functional directed multigraph using in-memory storage.
|
|
6
|
-
|
|
7
|
-
The implementation focuses on the operations exercised by ``ResearchSynthesizer``:
|
|
8
|
-
|
|
9
|
-
* ``upsert_entity`` – register/update an entity node with typed metadata
|
|
10
|
-
* ``upsert_relationship`` – connect two entities with rich relationship properties
|
|
11
|
-
* ``get_entity`` / ``get_relationships`` – helper APIs for diagnostics and future features
|
|
12
|
-
|
|
13
|
-
Data is persisted in memory and optionally mirrored to a JSON file on disk so the graph can
|
|
14
|
-
survive multiple sessions during local development. All public methods are ``async`` to keep
|
|
15
|
-
parity with the historical interface and to allow easy replacement with an external graph
|
|
16
|
-
backend in the future.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import asyncio
|
|
22
|
-
import json
|
|
23
|
-
from dataclasses import dataclass, field
|
|
24
|
-
from pathlib import Path
|
|
25
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
26
|
-
|
|
27
|
-
__all__ = ["KnowledgeGraph", "GraphEntity", "GraphRelationship"]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@dataclass
|
|
31
|
-
class GraphEntity:
|
|
32
|
-
"""Represents a node in the knowledge graph."""
|
|
33
|
-
|
|
34
|
-
entity_id: str
|
|
35
|
-
entity_type: str
|
|
36
|
-
properties: Dict[str, Any] = field(default_factory=dict)
|
|
37
|
-
|
|
38
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
39
|
-
return {
|
|
40
|
-
"id": self.entity_id,
|
|
41
|
-
"type": self.entity_type,
|
|
42
|
-
"properties": self.properties,
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@dataclass
|
|
47
|
-
class GraphRelationship:
|
|
48
|
-
"""Represents a directed, typed relationship between two entities."""
|
|
49
|
-
|
|
50
|
-
rel_type: str
|
|
51
|
-
source_id: str
|
|
52
|
-
target_id: str
|
|
53
|
-
properties: Dict[str, Any] = field(default_factory=dict)
|
|
54
|
-
|
|
55
|
-
def to_dict(self) -> Dict[str, Any]:
|
|
56
|
-
return {
|
|
57
|
-
"type": self.rel_type,
|
|
58
|
-
"source": self.source_id,
|
|
59
|
-
"target": self.target_id,
|
|
60
|
-
"properties": self.properties,
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class KnowledgeGraph:
|
|
65
|
-
"""A simple async-safe in-memory knowledge graph."""
|
|
66
|
-
|
|
67
|
-
def __init__(self, *, persistence_path: Optional[Path] = None) -> None:
|
|
68
|
-
self._entities: Dict[str, GraphEntity] = {}
|
|
69
|
-
# Adjacency list keyed by (source_id, rel_type) -> list[target_id, props]
|
|
70
|
-
self._relationships: List[GraphRelationship] = []
|
|
71
|
-
self._lock = asyncio.Lock()
|
|
72
|
-
self._persistence_path = persistence_path
|
|
73
|
-
if self._persistence_path:
|
|
74
|
-
self._load_from_disk()
|
|
75
|
-
|
|
76
|
-
# ------------------------------------------------------------------
|
|
77
|
-
# Persistence helpers
|
|
78
|
-
# ------------------------------------------------------------------
|
|
79
|
-
def _load_from_disk(self) -> None:
|
|
80
|
-
if not self._persistence_path or not self._persistence_path.exists():
|
|
81
|
-
return
|
|
82
|
-
try:
|
|
83
|
-
payload = json.loads(self._persistence_path.read_text())
|
|
84
|
-
except Exception:
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
for entity in payload.get("entities", []):
|
|
88
|
-
graph_entity = GraphEntity(
|
|
89
|
-
entity_id=entity["id"],
|
|
90
|
-
entity_type=entity.get("type", "Unknown"),
|
|
91
|
-
properties=entity.get("properties", {}),
|
|
92
|
-
)
|
|
93
|
-
self._entities[graph_entity.entity_id] = graph_entity
|
|
94
|
-
|
|
95
|
-
for rel in payload.get("relationships", []):
|
|
96
|
-
graph_rel = GraphRelationship(
|
|
97
|
-
rel_type=rel.get("type", "related_to"),
|
|
98
|
-
source_id=rel.get("source"),
|
|
99
|
-
target_id=rel.get("target"),
|
|
100
|
-
properties=rel.get("properties", {}),
|
|
101
|
-
)
|
|
102
|
-
self._relationships.append(graph_rel)
|
|
103
|
-
|
|
104
|
-
def _persist(self) -> None:
|
|
105
|
-
if not self._persistence_path:
|
|
106
|
-
return
|
|
107
|
-
data = {
|
|
108
|
-
"entities": [entity.to_dict() for entity in self._entities.values()],
|
|
109
|
-
"relationships": [rel.to_dict() for rel in self._relationships],
|
|
110
|
-
}
|
|
111
|
-
try:
|
|
112
|
-
self._persistence_path.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
-
self._persistence_path.write_text(json.dumps(data, indent=2, sort_keys=True))
|
|
114
|
-
except Exception:
|
|
115
|
-
# Persistence failures should never stop the conversation flow
|
|
116
|
-
pass
|
|
117
|
-
|
|
118
|
-
# ------------------------------------------------------------------
|
|
119
|
-
# Public API
|
|
120
|
-
# ------------------------------------------------------------------
|
|
121
|
-
async def upsert_entity(self, entity_type: str, properties: Dict[str, Any]) -> str:
|
|
122
|
-
"""Create or update an entity.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
entity_type: Semantic type (e.g., "Paper", "Author").
|
|
126
|
-
properties: Arbitrary metadata. ``properties['id']`` is optional; when missing
|
|
127
|
-
a deterministic identifier is derived from ``properties['external_id']`` or
|
|
128
|
-
a hash of the payload.
|
|
129
|
-
Returns:
|
|
130
|
-
The entity identifier stored in the graph.
|
|
131
|
-
"""
|
|
132
|
-
|
|
133
|
-
async with self._lock:
|
|
134
|
-
entity_id = _determine_entity_id(entity_type, properties)
|
|
135
|
-
entity = self._entities.get(entity_id)
|
|
136
|
-
if entity:
|
|
137
|
-
entity.properties.update(properties)
|
|
138
|
-
else:
|
|
139
|
-
entity = GraphEntity(entity_id=entity_id, entity_type=entity_type, properties=properties)
|
|
140
|
-
self._entities[entity_id] = entity
|
|
141
|
-
self._persist()
|
|
142
|
-
return entity_id
|
|
143
|
-
|
|
144
|
-
async def upsert_relationship(
|
|
145
|
-
self,
|
|
146
|
-
rel_type: str,
|
|
147
|
-
source_id: str,
|
|
148
|
-
target_id: str,
|
|
149
|
-
properties: Optional[Dict[str, Any]] = None,
|
|
150
|
-
) -> Tuple[str, str, str]:
|
|
151
|
-
"""Create or update a directed relationship between two entities."""
|
|
152
|
-
|
|
153
|
-
properties = properties or {}
|
|
154
|
-
async with self._lock:
|
|
155
|
-
relationship = GraphRelationship(
|
|
156
|
-
rel_type=rel_type,
|
|
157
|
-
source_id=source_id,
|
|
158
|
-
target_id=target_id,
|
|
159
|
-
properties=properties,
|
|
160
|
-
)
|
|
161
|
-
self._relationships.append(relationship)
|
|
162
|
-
self._persist()
|
|
163
|
-
return (relationship.rel_type, relationship.source_id, relationship.target_id)
|
|
164
|
-
|
|
165
|
-
async def get_entity(self, entity_id: str) -> Optional[GraphEntity]:
|
|
166
|
-
async with self._lock:
|
|
167
|
-
return self._entities.get(entity_id)
|
|
168
|
-
|
|
169
|
-
async def get_relationships(self, entity_id: str) -> List[GraphRelationship]:
|
|
170
|
-
async with self._lock:
|
|
171
|
-
return [rel for rel in self._relationships if rel.source_id == entity_id or rel.target_id == entity_id]
|
|
172
|
-
|
|
173
|
-
async def stats(self) -> Dict[str, Any]:
|
|
174
|
-
async with self._lock:
|
|
175
|
-
return {
|
|
176
|
-
"entities": len(self._entities),
|
|
177
|
-
"relationships": len(self._relationships),
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def _determine_entity_id(entity_type: str, properties: Dict[str, Any]) -> str:
|
|
182
|
-
"""Best-effort deterministic identifier for an entity."""
|
|
183
|
-
|
|
184
|
-
# Preferred explicit IDs
|
|
185
|
-
for key in ("id", "external_id", "paper_id", "author_id", "identifier"):
|
|
186
|
-
value = properties.get(key)
|
|
187
|
-
if value:
|
|
188
|
-
return str(value)
|
|
189
|
-
|
|
190
|
-
# Fall back to hashed representation (order-stable via JSON dumps)
|
|
191
|
-
import hashlib
|
|
192
|
-
|
|
193
|
-
payload = json.dumps({"type": entity_type, "properties": properties}, sort_keys=True)
|
|
194
|
-
return f"{entity_type}:{hashlib.md5(payload.encode('utf-8')).hexdigest()}"
|