deploy_check.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. """Read-only deployment diagnostics; never print credentials or provider payloads."""
  2. import json
  3. from collections import Counter
  4. from pathlib import Path
  5. def result(ok, code, **details):
  6. return {'ok': bool(ok), 'code': code, **details}
  7. def milvus(root, client_factory=None):
  8. from src.step2_knowledge_ingest.ingest import config, content_parts, load, route
  9. settings = config(root / '.env')
  10. manifest_path = root / 'knowledge/prepared/ingestion/manifest.json'
  11. if not manifest_path.is_file():
  12. return result(False, 'manifest_missing')
  13. manifest = json.loads(manifest_path.read_text(encoding='utf-8'))
  14. if manifest.get('complete') is not True or not isinstance(manifest.get('databases'), dict):
  15. return result(False, 'manifest_incomplete')
  16. expected_segments = Counter()
  17. local_records = Counter()
  18. for record in load(root / 'knowledge/prepared'):
  19. kind = route(record)
  20. expected_segments[kind] += len(content_parts(record))
  21. local_records[kind] += 1
  22. if dict(local_records) != manifest['counts'] or set(manifest['databases']) != set(local_records):
  23. return result(False, 'manifest_local_mismatch')
  24. if client_factory is None:
  25. from pymilvus import MilvusClient
  26. client_factory = MilvusClient
  27. client = client_factory(uri=settings['MILVUS_URI'], timeout=settings['MILVUS_TIMEOUT_SECONDS'])
  28. timeout = settings['MILVUS_TIMEOUT_SECONDS']
  29. available = set(client.list_databases(timeout=timeout))
  30. libraries = {}
  31. for kind, spec in manifest['databases'].items():
  32. expected = manifest['counts'].get(kind)
  33. database = spec['database']
  34. if database != settings['MILVUS_DB_PREFIX'] + '_' + kind or database not in available:
  35. libraries[kind] = result(False, 'database_missing_or_mismatched', expected_records=expected)
  36. continue
  37. client.use_database(database, timeout=timeout)
  38. names = set(client.list_collections(timeout=timeout))
  39. index_name, content_name = spec['index_collection'], spec['content_collection']
  40. if index_name not in names or content_name not in names:
  41. libraries[kind] = result(False, 'collection_missing', expected_records=expected)
  42. continue
  43. def count(name):
  44. return client.query(name, filter='', output_fields=['count(*)'],
  45. consistency_level='Strong', timeout=timeout)[0]['count(*)']
  46. indexed, segments = count(index_name), count(content_name)
  47. expected_parts = expected_segments[kind]
  48. ok = indexed == expected and segments == expected_parts
  49. libraries[kind] = result(ok, 'matched' if ok else 'count_mismatch',
  50. expected_records=expected, indexed_records=indexed,
  51. expected_content_segments=expected_parts, content_segments=segments,
  52. index_delta=indexed - expected, content_delta=segments - expected_parts)
  53. return result(all(row['ok'] for row in libraries.values()) and bool(libraries),
  54. 'matched' if all(row['ok'] for row in libraries.values()) and libraries else 'knowledge_difference',
  55. snapshot=manifest.get('snapshot'), libraries=libraries,
  56. expected_total=sum(manifest['counts'].values()),
  57. indexed_total=sum(row.get('indexed_records', 0) for row in libraries.values()))
  58. def redis_check(root, client_factory=None):
  59. from src.step3_question_answer.context import load_context_config
  60. settings = load_context_config(root / '.env')
  61. if client_factory is None:
  62. from redis import Redis
  63. client_factory = Redis.from_url
  64. client = client_factory(settings.url, socket_connect_timeout=5, socket_timeout=5,
  65. decode_responses=False, protocol=2)
  66. try:
  67. connected = bool(client.ping())
  68. return result(connected, 'connected' if connected else 'ping_failed')
  69. finally:
  70. client.close()
  71. def dms(root, session_factory=None, company_factory=None):
  72. """Verify the five DMS columns used by the app through read-only metadata calls."""
  73. from src.step5_session_analysis.dms import DmsClient, load_dms_config
  74. from src.step3_question_answer.dms_company import DmsCompanyClient
  75. from src.dms_schema import names as schema_names
  76. session_factory = session_factory or DmsClient
  77. company_factory = company_factory or DmsCompanyClient
  78. schema_path = root / 'config/dms_schema.json'
  79. checks = {}
  80. try:
  81. session = session_factory(load_dms_config(root / '.env', schema_path=schema_path))
  82. session.connect()
  83. checks['sessions'] = result(True, 'usable', columns=3)
  84. except Exception as exc:
  85. checks['sessions'] = result(False, safe_code(exc))
  86. try:
  87. from src.dms_schema import load_schema
  88. schema = load_schema(schema_path)
  89. config = load_dms_config(root / '.env', scope_names=schema_names(schema, ('basic', 'honor')),
  90. schema_path=schema_path)
  91. company = company_factory(config)
  92. company.connect(config.schema['columns']['basic']['id'])
  93. company.connect(config.schema['columns']['honor']['id'])
  94. checks['company'] = result(True, 'usable', columns=2)
  95. except Exception as exc:
  96. checks['company'] = result(False, safe_code(exc))
  97. healthy = all(row['ok'] for row in checks.values())
  98. return result(healthy, 'usable' if healthy else 'api_failure', scopes=checks)
  99. def deepseek(root, send=None):
  100. from src.step1_knowledge_prepare.summarize import load_config, transport
  101. config = load_config(root / '.env')
  102. payload = {'model': config.model, 'messages': [{'role': 'user', 'content': 'Reply with OK.'}],
  103. 'max_tokens': 128, 'stream': False}
  104. response = (send or transport)(payload, config)
  105. choices = response.get('choices') if isinstance(response, dict) else None
  106. content = choices[0].get('message', {}).get('content') if isinstance(choices, list) and choices and isinstance(choices[0], dict) else None
  107. return result(isinstance(content, str) and bool(content.strip()),
  108. 'usable' if isinstance(content, str) and content.strip() else 'empty_model_response')
  109. def qcc(root, company_name, client=None):
  110. from src.step3_question_answer.qcc import QccClient, load_config
  111. client = client or QccClient(load_config(root / '.env'))
  112. checks = {}
  113. try:
  114. search = client.search(company_name)
  115. checks['886'] = result(True, 'usable', candidates=len(search['candidates']))
  116. except Exception as exc:
  117. checks['886'] = result(False, safe_code(exc))
  118. search = None
  119. exact = [row for row in search['candidates'] if row['Name'] == company_name] if search else []
  120. if len(exact) != 1:
  121. code = 'exact_company_not_found' if search else 'search_failed'
  122. checks['410'] = result(False, code)
  123. checks['1001'] = result(False, code)
  124. else:
  125. company = exact[0]
  126. try:
  127. client.basic(company)
  128. checks['410'] = result(True, 'usable')
  129. except Exception as exc:
  130. checks['410'] = result(False, safe_code(exc))
  131. try:
  132. honors = client.honors(company)
  133. checks['1001'] = result(honors['complete'], 'usable' if honors['complete'] else safe_code(Exception(honors['error'])),
  134. records=len(honors['records']), pages=len(honors['sources']))
  135. except Exception as exc:
  136. checks['1001'] = result(False, safe_code(exc))
  137. return result(all(row['ok'] for row in checks.values()), 'usable' if all(row['ok'] for row in checks.values()) else 'api_failure',
  138. interfaces=checks)
  139. def safe_code(exc):
  140. code = str(exc)
  141. allowed = {'provider_failure', 'qcc_auth_failed', 'invalid_qcc_configuration', 'invalid_result', 'invalid_search_result', 'invalid_candidate',
  142. 'duplicate_candidate', 'company_identity_mismatch', 'invalid_honor_result',
  143. 'invalid_honor_data', 'inconsistent_honor_result', 'inconsistent_honor_paging',
  144. 'page_limit', 'network_failure', 'invalid_json', 'response_too_large',
  145. 'invalid_dms_configuration', 'invalid_dms_login_url', 'dms_scope_denied', 'dms_redirect_refused',
  146. 'dms_transport_failed', 'dms_invalid_envelope', 'dms_permission_denied',
  147. 'dms_token_invalid', 'dms_service_failed', 'dms_auth_retry_later',
  148. 'dms_login_failed', 'dms_direct_api_requires_token', 'dms_column_mismatch',
  149. 'dms_parent_mismatch', 'dms_schema_mismatch', 'dms_write_scope_denied',
  150. 'dms_operation_denied', 'invalid_deepseek_base_url', 'missing_deepseek_configuration',
  151. 'request_timeout', 'response_incomplete', 'invalid_response_json'}
  152. return code if code in allowed or (code.startswith('http_') and code[5:].isdigit()) else 'request_failed'
  153. def run(root, company_name=None, action='check'):
  154. root = Path(root)
  155. checks = {}
  156. probes = {'milvus': lambda: milvus(root), 'redis': lambda: redis_check(root),
  157. 'dms': lambda: dms(root),
  158. 'deepseek': lambda: deepseek(root), 'qcc': lambda: qcc(root, company_name)}
  159. names = tuple(probes) if action == 'check' else (action.removeprefix('check-'),)
  160. if not names or any(name not in probes for name in names):
  161. raise ValueError('invalid_check_action')
  162. for name in names:
  163. try:
  164. checks[name] = probes[name]()
  165. except Exception as exc:
  166. checks[name] = result(False, safe_code(exc))
  167. report = {'ok': all(row['ok'] for row in checks.values()), 'checks': checks}
  168. print(json.dumps(report, ensure_ascii=False), flush=True)
  169. return 0 if report['ok'] else 1