| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- """Read-only deployment diagnostics; never print credentials or provider payloads."""
- import json
- from collections import Counter
- from pathlib import Path
- def result(ok, code, **details):
- return {'ok': bool(ok), 'code': code, **details}
- def milvus(root, client_factory=None):
- from src.step2_knowledge_ingest.ingest import config, content_parts, load, route
- settings = config(root / '.env')
- manifest_path = root / 'knowledge/prepared/ingestion/manifest.json'
- if not manifest_path.is_file():
- return result(False, 'manifest_missing')
- manifest = json.loads(manifest_path.read_text(encoding='utf-8'))
- if manifest.get('complete') is not True or not isinstance(manifest.get('databases'), dict):
- return result(False, 'manifest_incomplete')
- expected_segments = Counter()
- local_records = Counter()
- for record in load(root / 'knowledge/prepared'):
- kind = route(record)
- expected_segments[kind] += len(content_parts(record))
- local_records[kind] += 1
- if dict(local_records) != manifest['counts'] or set(manifest['databases']) != set(local_records):
- return result(False, 'manifest_local_mismatch')
- if client_factory is None:
- from pymilvus import MilvusClient
- client_factory = MilvusClient
- client = client_factory(uri=settings['MILVUS_URI'], timeout=settings['MILVUS_TIMEOUT_SECONDS'])
- timeout = settings['MILVUS_TIMEOUT_SECONDS']
- available = set(client.list_databases(timeout=timeout))
- libraries = {}
- for kind, spec in manifest['databases'].items():
- expected = manifest['counts'].get(kind)
- database = spec['database']
- if database != settings['MILVUS_DB_PREFIX'] + '_' + kind or database not in available:
- libraries[kind] = result(False, 'database_missing_or_mismatched', expected_records=expected)
- continue
- client.use_database(database, timeout=timeout)
- names = set(client.list_collections(timeout=timeout))
- index_name, content_name = spec['index_collection'], spec['content_collection']
- if index_name not in names or content_name not in names:
- libraries[kind] = result(False, 'collection_missing', expected_records=expected)
- continue
- def count(name):
- return client.query(name, filter='', output_fields=['count(*)'],
- consistency_level='Strong', timeout=timeout)[0]['count(*)']
- indexed, segments = count(index_name), count(content_name)
- expected_parts = expected_segments[kind]
- ok = indexed == expected and segments == expected_parts
- libraries[kind] = result(ok, 'matched' if ok else 'count_mismatch',
- expected_records=expected, indexed_records=indexed,
- expected_content_segments=expected_parts, content_segments=segments,
- index_delta=indexed - expected, content_delta=segments - expected_parts)
- return result(all(row['ok'] for row in libraries.values()) and bool(libraries),
- 'matched' if all(row['ok'] for row in libraries.values()) and libraries else 'knowledge_difference',
- snapshot=manifest.get('snapshot'), libraries=libraries,
- expected_total=sum(manifest['counts'].values()),
- indexed_total=sum(row.get('indexed_records', 0) for row in libraries.values()))
- def redis_check(root, client_factory=None):
- from src.step3_question_answer.context import load_context_config
- settings = load_context_config(root / '.env')
- if client_factory is None:
- from redis import Redis
- client_factory = Redis.from_url
- client = client_factory(settings.url, socket_connect_timeout=5, socket_timeout=5,
- decode_responses=False, protocol=2)
- try:
- connected = bool(client.ping())
- return result(connected, 'connected' if connected else 'ping_failed')
- finally:
- client.close()
- def dms(root, session_factory=None, company_factory=None):
- """Verify the five DMS columns used by the app through read-only metadata calls."""
- from src.step5_session_analysis.dms import DmsClient, load_dms_config
- from src.step3_question_answer.dms_company import DmsCompanyClient
- from src.dms_schema import names as schema_names
- session_factory = session_factory or DmsClient
- company_factory = company_factory or DmsCompanyClient
- schema_path = root / 'config/dms_schema.json'
- checks = {}
- try:
- session = session_factory(load_dms_config(root / '.env', schema_path=schema_path))
- session.connect()
- checks['sessions'] = result(True, 'usable', columns=3)
- except Exception as exc:
- checks['sessions'] = result(False, safe_code(exc))
- try:
- from src.dms_schema import load_schema
- schema = load_schema(schema_path)
- config = load_dms_config(root / '.env', scope_names=schema_names(schema, ('basic', 'honor')),
- schema_path=schema_path)
- company = company_factory(config)
- company.connect(config.schema['columns']['basic']['id'])
- company.connect(config.schema['columns']['honor']['id'])
- checks['company'] = result(True, 'usable', columns=2)
- except Exception as exc:
- checks['company'] = result(False, safe_code(exc))
- healthy = all(row['ok'] for row in checks.values())
- return result(healthy, 'usable' if healthy else 'api_failure', scopes=checks)
- def deepseek(root, send=None):
- from src.step1_knowledge_prepare.summarize import load_config, transport
- config = load_config(root / '.env')
- payload = {'model': config.model, 'messages': [{'role': 'user', 'content': 'Reply with OK.'}],
- 'max_tokens': 16, 'stream': False}
- response = (send or transport)(payload, config)
- choices = response.get('choices') if isinstance(response, dict) else None
- content = choices[0].get('message', {}).get('content') if isinstance(choices, list) and choices and isinstance(choices[0], dict) else None
- return result(isinstance(content, str) and bool(content.strip()),
- 'usable' if isinstance(content, str) and content.strip() else 'empty_model_response')
- def qcc(root, company_name, client=None):
- from src.step3_question_answer.qcc import QccClient, load_config
- client = client or QccClient(load_config(root / '.env'))
- checks = {}
- try:
- search = client.search(company_name)
- checks['886'] = result(True, 'usable', candidates=len(search['candidates']))
- except Exception as exc:
- checks['886'] = result(False, safe_code(exc))
- search = None
- exact = [row for row in search['candidates'] if row['Name'] == company_name] if search else []
- if len(exact) != 1:
- code = 'exact_company_not_found' if search else 'search_failed'
- checks['410'] = result(False, code)
- checks['1001'] = result(False, code)
- else:
- company = exact[0]
- try:
- client.basic(company)
- checks['410'] = result(True, 'usable')
- except Exception as exc:
- checks['410'] = result(False, safe_code(exc))
- try:
- honors = client.honors(company)
- checks['1001'] = result(honors['complete'], 'usable' if honors['complete'] else safe_code(Exception(honors['error'])),
- records=len(honors['records']), pages=len(honors['sources']))
- except Exception as exc:
- checks['1001'] = result(False, safe_code(exc))
- return result(all(row['ok'] for row in checks.values()), 'usable' if all(row['ok'] for row in checks.values()) else 'api_failure',
- interfaces=checks)
- def safe_code(exc):
- code = str(exc)
- allowed = {'provider_failure', 'invalid_result', 'invalid_search_result', 'invalid_candidate',
- 'duplicate_candidate', 'company_identity_mismatch', 'invalid_honor_result',
- 'invalid_honor_data', 'inconsistent_honor_result', 'inconsistent_honor_paging',
- 'page_limit', 'network_failure', 'invalid_json', 'response_too_large',
- 'invalid_dms_configuration', 'dms_scope_denied', 'dms_redirect_refused',
- 'dms_transport_failed', 'dms_invalid_envelope', 'dms_permission_denied',
- 'dms_token_invalid', 'dms_service_failed', 'dms_auth_retry_later',
- 'dms_login_failed', 'dms_direct_api_requires_token', 'dms_column_mismatch',
- 'dms_parent_mismatch', 'dms_schema_mismatch', 'dms_write_scope_denied',
- 'dms_operation_denied'}
- return code if code in allowed or (code.startswith('http_') and code[5:].isdigit()) else 'request_failed'
- def run(root, company_name=None, action='check'):
- root = Path(root)
- checks = {}
- probes = {'milvus': lambda: milvus(root), 'redis': lambda: redis_check(root),
- 'dms': lambda: dms(root),
- 'deepseek': lambda: deepseek(root), 'qcc': lambda: qcc(root, company_name)}
- names = tuple(probes) if action == 'check' else (action.removeprefix('check-'),)
- if not names or any(name not in probes for name in names):
- raise ValueError('invalid_check_action')
- for name in names:
- try:
- checks[name] = probes[name]()
- except Exception as exc:
- checks[name] = result(False, safe_code(exc))
- report = {'ok': all(row['ok'] for row in checks.values()), 'checks': checks}
- print(json.dumps(report, ensure_ascii=False), flush=True)
- return 0 if report['ok'] else 1
|