veadk-python 1.0.2__py3-none-any.whl → 1.0.3__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.
@@ -22,6 +22,7 @@ import os
22
22
  import re
23
23
  import time
24
24
  import uuid
25
+ from collections.abc import Mapping
25
26
  from dataclasses import dataclass
26
27
  from datetime import datetime, timezone
27
28
  from typing import Any
@@ -30,6 +31,7 @@ from urllib.parse import quote, urlparse, urlunparse
30
31
  import requests
31
32
 
32
33
  from veadk.auth.veauth.utils import get_credential_from_vefaas_iam
34
+ from veadk.utils.auth import VE_TIP_TOKEN_HEADER
33
35
 
34
36
  DEFAULT_ENDPOINT = "http://volcengineapi.byted.org/"
35
37
  DEFAULT_VERSION = "2025-10-30"
@@ -66,6 +68,8 @@ class AgentKitA2ARegistryConfig:
66
68
  top_k: int = DEFAULT_TOP_K
67
69
  timeout_ms: int = DEFAULT_TIMEOUT_MS
68
70
  poll_interval_ms: int = DEFAULT_POLL_INTERVAL_MS
71
+ upstream_tip_token: str = ""
72
+ upstream_authorization: str = ""
69
73
 
70
74
 
71
75
  @dataclass(frozen=True)
@@ -97,9 +101,41 @@ def registry_config_from_env() -> AgentKitA2ARegistryConfig:
97
101
  poll_interval_ms=_int_env(
98
102
  "REGISTRY_POLL_INTERVAL_MS", DEFAULT_POLL_INTERVAL_MS, minimum=100
99
103
  ),
104
+ upstream_tip_token=_first_env(
105
+ [
106
+ "REGISTRY_UPSTREAM_TIP_TOKEN",
107
+ "AGENTKIT_UPSTREAM_TIP_TOKEN",
108
+ "A2A_REGISTRY_UPSTREAM_TIP_TOKEN",
109
+ "VE_TIP_TOKEN",
110
+ "X_VE_TIP_TOKEN",
111
+ "TIP_TOKEN",
112
+ ]
113
+ ),
100
114
  )
101
115
 
102
116
 
117
+ def registry_tip_token_from_headers(headers: Mapping[str, str]) -> str:
118
+ """Extract an inbound TIP token from HTTP headers."""
119
+
120
+ normalized = {str(key).lower(): str(value) for key, value in headers.items()}
121
+ return normalized.get(VE_TIP_TOKEN_HEADER.lower(), "").strip()
122
+
123
+
124
+ def registry_authorization_from_headers(headers: Mapping[str, str]) -> str:
125
+ """Extract a bearer Authorization header for downstream A2A calls."""
126
+
127
+ normalized = {str(key).lower(): str(value) for key, value in headers.items()}
128
+ for header_name in (
129
+ "authorization",
130
+ "x-forwarded-authorization",
131
+ "x-original-authorization",
132
+ ):
133
+ value = _normalize_bearer_authorization(normalized.get(header_name, ""))
134
+ if value:
135
+ return value
136
+ return ""
137
+
138
+
103
139
  def search_agent_cards(
104
140
  prompt: str,
105
141
  top_k: int | None = None,
@@ -250,6 +286,12 @@ def _resolve_config(
250
286
  or DEFAULT_POLL_INTERVAL_MS
251
287
  ),
252
288
  ),
289
+ upstream_tip_token=(
290
+ config.upstream_tip_token or env_config.upstream_tip_token
291
+ ).strip(),
292
+ upstream_authorization=_normalize_bearer_authorization(
293
+ config.upstream_authorization or env_config.upstream_authorization
294
+ ),
253
295
  )
254
296
 
255
297
 
@@ -534,15 +576,25 @@ def _send_message(
534
576
  if task_id:
535
577
  message["taskId"] = task_id
536
578
 
579
+ headers: dict[str, str] = {}
537
580
  try:
581
+ headers = _agent_auth_headers(card, config)
538
582
  return _a2a_jsonrpc(
539
583
  card["url"],
540
584
  "message/send",
541
585
  {"message": message, "configuration": {"blocking": False}},
542
- _agent_auth_headers(card, config),
586
+ headers,
543
587
  config,
544
588
  )
545
589
  except RegistryError as exc:
590
+ if _should_retry_with_m2m(exc, card, config, headers):
591
+ return _a2a_jsonrpc(
592
+ card["url"],
593
+ "message/send",
594
+ {"message": message, "configuration": {"blocking": False}},
595
+ _agent_auth_headers(card, config, force_m2m=True),
596
+ config,
597
+ )
546
598
  if exc.code in {
547
599
  "A2A_HTTP_FAILED",
548
600
  "A2A_RESPONSE_PARSE_FAILED",
@@ -563,13 +615,25 @@ def _poll_card(
563
615
  started: float | None = None,
564
616
  ) -> dict[str, Any]:
565
617
  started = started or time.monotonic()
566
- a2a_result = _a2a_jsonrpc(
567
- card["url"],
568
- "tasks/get",
569
- {"id": task_id.strip(), "historyLength": max(0, int(history_length))},
570
- _agent_auth_headers(card, config),
571
- config,
572
- )
618
+ headers = _agent_auth_headers(card, config)
619
+ try:
620
+ a2a_result = _a2a_jsonrpc(
621
+ card["url"],
622
+ "tasks/get",
623
+ {"id": task_id.strip(), "historyLength": max(0, int(history_length))},
624
+ headers,
625
+ config,
626
+ )
627
+ except RegistryError as exc:
628
+ if not _should_retry_with_m2m(exc, card, config, headers):
629
+ raise
630
+ a2a_result = _a2a_jsonrpc(
631
+ card["url"],
632
+ "tasks/get",
633
+ {"id": task_id.strip(), "historyLength": max(0, int(history_length))},
634
+ _agent_auth_headers(card, config, force_m2m=True),
635
+ config,
636
+ )
573
637
  state = _task_state(a2a_result)
574
638
  is_terminal = state in TERMINAL_STATES
575
639
  payload: dict[str, Any] = {
@@ -815,11 +879,18 @@ def _sanitize_get_agent_result(
815
879
 
816
880
 
817
881
  def _agent_auth_headers(
818
- card: dict[str, Any], config: AgentKitA2ARegistryConfig | None = None
882
+ card: dict[str, Any],
883
+ config: AgentKitA2ARegistryConfig | None = None,
884
+ *,
885
+ force_m2m: bool = False,
819
886
  ) -> dict[str, str]:
887
+ resolved_config = _resolve_config(config)
820
888
  security = card.get("security") or []
821
889
  schemes = card.get("securitySchemes") or {}
822
890
  headers: dict[str, str] = {}
891
+ upstream_authorization = _normalize_bearer_authorization(
892
+ resolved_config.upstream_authorization
893
+ )
823
894
 
824
895
  for requirement in security:
825
896
  if not isinstance(requirement, dict):
@@ -837,9 +908,17 @@ def _agent_auth_headers(
837
908
  if isinstance(token, str) and token:
838
909
  headers[header_name] = token
839
910
  elif scheme_type == "oauth2":
840
- headers["Authorization"] = "Bearer " + _oauth2_client_credentials_token(
841
- scheme, _resolve_config(config)
842
- )
911
+ if upstream_authorization and not force_m2m:
912
+ headers["Authorization"] = upstream_authorization
913
+ else:
914
+ headers["Authorization"] = (
915
+ "Bearer "
916
+ + _oauth2_client_credentials_token(scheme, resolved_config)
917
+ )
918
+
919
+ tip_token = resolved_config.upstream_tip_token
920
+ if tip_token:
921
+ headers[VE_TIP_TOKEN_HEADER] = tip_token
843
922
 
844
923
  if security and not headers:
845
924
  raise RegistryError(
@@ -849,6 +928,38 @@ def _agent_auth_headers(
849
928
  return headers
850
929
 
851
930
 
931
+ def _should_retry_with_m2m(
932
+ exc: RegistryError,
933
+ card: dict[str, Any],
934
+ config: AgentKitA2ARegistryConfig | None,
935
+ headers: dict[str, str],
936
+ ) -> bool:
937
+ if exc.code != "A2A_HTTP_FAILED" or exc.diagnostics.get("status_code") != 401:
938
+ return False
939
+
940
+ resolved_config = _resolve_config(config)
941
+ upstream_authorization = _normalize_bearer_authorization(
942
+ resolved_config.upstream_authorization
943
+ )
944
+ if not upstream_authorization:
945
+ return False
946
+ if headers.get("Authorization") != upstream_authorization:
947
+ return False
948
+ return _has_oauth2_security(card)
949
+
950
+
951
+ def _has_oauth2_security(card: dict[str, Any]) -> bool:
952
+ schemes = card.get("securitySchemes") or {}
953
+ for requirement in card.get("security") or []:
954
+ if not isinstance(requirement, dict):
955
+ continue
956
+ for scheme_name in requirement:
957
+ scheme = schemes.get(scheme_name) or {}
958
+ if str(scheme.get("type") or "").lower() == "oauth2":
959
+ return True
960
+ return False
961
+
962
+
852
963
  def _oauth2_client_credentials_token(
853
964
  scheme: dict[str, Any], config: AgentKitA2ARegistryConfig
854
965
  ) -> str:
@@ -957,6 +1068,16 @@ def _clean_config_url(value: Any) -> str:
957
1068
  return cleaned
958
1069
 
959
1070
 
1071
+ def _normalize_bearer_authorization(value: Any) -> str:
1072
+ cleaned = _clean_config_url(value)
1073
+ if not cleaned:
1074
+ return ""
1075
+ parts = cleaned.split(None, 1)
1076
+ if len(parts) == 2 and parts[0].lower() == "bearer" and parts[1].strip():
1077
+ return f"Bearer {parts[1].strip()}"
1078
+ return ""
1079
+
1080
+
960
1081
  def _user_pool_id_from_token_url(token_url: str) -> str:
961
1082
  host = urlparse(token_url).netloc
962
1083
  match = re.match(r"userpool-([^.]+)\.userpool\.auth\.id\.", host)
@@ -62,6 +62,10 @@ from google.adk.utils.context_utils import Aclosing
62
62
  from typing_extensions import override
63
63
 
64
64
  from veadk import Agent
65
+ from veadk.a2a.registry_client import (
66
+ registry_authorization_from_headers,
67
+ registry_tip_token_from_headers,
68
+ )
65
69
  from veadk.a2a.utils.agent_to_a2a import to_a2a
66
70
  from veadk.cloud.harness_app.agent import agent, short_term_memory
67
71
  from veadk.cloud.harness_app.harness_plugins import (
@@ -229,6 +233,8 @@ class HarnessApp:
229
233
  )
230
234
 
231
235
  try:
236
+ tip_token = registry_tip_token_from_headers(http_request.headers)
237
+ auth_header = registry_authorization_from_headers(http_request.headers)
232
238
  header_plugins = build_harness_plugins_from_headers(
233
239
  http_request.headers
234
240
  )
@@ -269,6 +275,8 @@ class HarnessApp:
269
275
  request.prompt,
270
276
  request.harness,
271
277
  download_dir=Path(work_dir),
278
+ registry_tip_token=tip_token,
279
+ registry_authorization=auth_header,
272
280
  )
273
281
  runner = Runner(
274
282
  agent=agent,
@@ -283,11 +291,15 @@ class HarnessApp:
283
291
  run_config=run_config,
284
292
  )
285
293
  elif needs_scoped_runner:
286
- run_agent = (
287
- spawn_harness_run_agent(self.agent, request.prompt)
288
- if has_registry
289
- else self.agent
290
- )
294
+ if has_registry:
295
+ run_agent = spawn_harness_run_agent(
296
+ self.agent,
297
+ request.prompt,
298
+ registry_tip_token=tip_token,
299
+ registry_authorization=auth_header,
300
+ )
301
+ else:
302
+ run_agent = self.agent
291
303
  runner = Runner(
292
304
  agent=run_agent,
293
305
  short_term_memory=self.short_term_memory,
@@ -359,7 +371,7 @@ class HarnessApp:
359
371
  break
360
372
 
361
373
  @self.app.post("/run_sse")
362
- async def run_sse(req: HarnessRunAgentRequest):
374
+ async def run_sse(req: HarnessRunAgentRequest, http_request: Request):
363
375
  if (
364
376
  req.harness is None
365
377
  and not has_a2a_registry_config(self.agent)
@@ -367,8 +379,11 @@ class HarnessApp:
367
379
  ):
368
380
  # No override -> exactly ADK's default /run_sse.
369
381
  return await adk_run_sse(req)
382
+ tip_token = registry_tip_token_from_headers(http_request.headers)
383
+ auth_header = registry_authorization_from_headers(http_request.headers)
370
384
  return StreamingResponse(
371
- self._run_sse_events(req), media_type="text/event-stream"
385
+ self._run_sse_events(req, tip_token, auth_header),
386
+ media_type="text/event-stream",
372
387
  )
373
388
 
374
389
  # Move ours to the front so it wins (Starlette matches the first route),
@@ -381,7 +396,12 @@ class HarnessApp:
381
396
  routes.insert(0, routes.pop(i))
382
397
  break
383
398
 
384
- async def _run_sse_events(self, req: "HarnessRunAgentRequest"):
399
+ async def _run_sse_events(
400
+ self,
401
+ req: "HarnessRunAgentRequest",
402
+ tip_token: str = "",
403
+ auth_header: str = "",
404
+ ):
385
405
  """Yield SSE ``data:`` lines for a run, spawning the agent on override."""
386
406
  run_config = RunConfig(
387
407
  streaming_mode=StreamingMode.SSE if req.streaming else StreamingMode.NONE
@@ -400,13 +420,20 @@ class HarnessApp:
400
420
  prompt,
401
421
  req.harness,
402
422
  download_dir=Path(work_dir_ctx.name),
423
+ registry_tip_token=tip_token,
424
+ registry_authorization=auth_header,
403
425
  )
404
426
  except (SkillLoadError, ToolLoadError) as e:
405
427
  logger.error(f"Once-time override failed to load: {e}")
406
428
  yield f"data: {json.dumps({'error': str(e)})}\n\n"
407
429
  return
408
430
  elif has_a2a_registry_config(self.agent):
409
- agent = spawn_harness_run_agent(self.agent, prompt)
431
+ agent = spawn_harness_run_agent(
432
+ self.agent,
433
+ prompt,
434
+ registry_tip_token=tip_token,
435
+ registry_authorization=auth_header,
436
+ )
410
437
  else:
411
438
  agent = self.agent
412
439
 
@@ -527,6 +527,30 @@ def _apply_registry_overrides(
527
527
  setattr(agent, _REGISTRY_CONFIG_ATTR, overridden_config)
528
528
 
529
529
 
530
+ def _apply_registry_request_auth(
531
+ agent: Agent, tip_token: str = "", authorization: str = ""
532
+ ) -> None:
533
+ cleaned_tip_token = (tip_token or "").strip()
534
+ cleaned_authorization = (authorization or "").strip()
535
+ if not cleaned_tip_token and not cleaned_authorization:
536
+ return
537
+
538
+ from veadk.tools.builtin_tools.a2a_registry import build_a2a_registry_tools
539
+
540
+ config = getattr(agent, _REGISTRY_CONFIG_ATTR, None)
541
+ if config is None:
542
+ return
543
+
544
+ updated_config = replace(
545
+ config,
546
+ upstream_tip_token=cleaned_tip_token or config.upstream_tip_token,
547
+ upstream_authorization=cleaned_authorization or config.upstream_authorization,
548
+ )
549
+ _remove_a2a_registry_tools(agent)
550
+ agent.tools.extend(build_a2a_registry_tools(updated_config))
551
+ setattr(agent, _REGISTRY_CONFIG_ATTR, updated_config)
552
+
553
+
530
554
  def has_a2a_registry_config(agent: Agent) -> bool:
531
555
  """Return whether ``agent`` has an AgentKit A2A registry configured."""
532
556
 
@@ -604,6 +628,8 @@ def spawn_harness_run_agent(
604
628
  prompt: str,
605
629
  overrides: HarnessOverrides | None = None,
606
630
  download_dir: Path | None = None,
631
+ registry_tip_token: str = "",
632
+ registry_authorization: str = "",
607
633
  ) -> Agent:
608
634
  """Clone a harness agent for one run and attach per-turn dynamic tools."""
609
635
 
@@ -612,5 +638,6 @@ def spawn_harness_run_agent(
612
638
  else:
613
639
  cloned = base_agent.clone(update={})
614
640
 
641
+ _apply_registry_request_auth(cloned, registry_tip_token, registry_authorization)
615
642
  _add_dynamic_a2a_agent_tools(cloned, prompt)
616
643
  return cloned
veadk/config.py CHANGED
@@ -21,6 +21,7 @@ from pydantic import BaseModel, Field
21
21
  from veadk.configs.auth_configs import VeIdentityConfig
22
22
  from veadk.configs.model_configs import RealtimeModelConfig
23
23
  from veadk.configs.database_configs import (
24
+ MilvusConfig,
24
25
  MysqlConfig,
25
26
  OpensearchConfig,
26
27
  RedisConfig,
@@ -79,6 +80,7 @@ class VeADKConfig(BaseModel):
79
80
  opensearch: OpensearchConfig = Field(default_factory=OpensearchConfig)
80
81
  mysql: MysqlConfig = Field(default_factory=MysqlConfig)
81
82
  redis: RedisConfig = Field(default_factory=RedisConfig)
83
+ milvus: MilvusConfig = Field(default_factory=MilvusConfig)
82
84
  viking_knowledgebase: VikingKnowledgebaseConfig = Field(
83
85
  default_factory=VikingKnowledgebaseConfig
84
86
  )
@@ -15,6 +15,7 @@
15
15
  import os
16
16
  from functools import cached_property
17
17
 
18
+ from pydantic import Field
18
19
  from pydantic_settings import BaseSettings, SettingsConfigDict
19
20
 
20
21
  from veadk.consts import DEFAULT_TOS_BUCKET_NAME
@@ -95,6 +96,26 @@ class RedisConfig(BaseSettings):
95
96
  """STS token for Redis auth, not supported yet."""
96
97
 
97
98
 
99
+ class MilvusConfig(BaseSettings):
100
+ model_config = SettingsConfigDict(env_prefix="DATABASE_MILVUS_")
101
+
102
+ uri: str = ""
103
+
104
+ token: str = ""
105
+
106
+ user: str = ""
107
+
108
+ password: str = ""
109
+
110
+ db_name: str = "default"
111
+
112
+ overwrite: bool = False
113
+
114
+ timeout: float | None = None
115
+
116
+ output_fields: list[str] | str = Field(default_factory=list)
117
+
118
+
98
119
  class Mem0Config(BaseSettings):
99
120
  model_config = SettingsConfigDict(env_prefix="DATABASE_MEM0_")
100
121
 
@@ -108,6 +129,16 @@ class Mem0Config(BaseSettings):
108
129
  base_url: str = "" # "https://api.mem0.ai/v1"
109
130
 
110
131
 
132
+ class OpenVikingConfig(BaseSettings):
133
+ model_config = SettingsConfigDict(env_prefix="DATABASE_OPENVIKING_")
134
+
135
+ url: str = ""
136
+ """OpenViking server base URL"""
137
+
138
+ api_key: str = ""
139
+ """OpenViking service owner user key"""
140
+
141
+
111
142
  class VikingKnowledgebaseConfig(BaseSettings):
112
143
  model_config = SettingsConfigDict(env_prefix="DATABASE_VIKING_")
113
144
 
@@ -0,0 +1,201 @@
1
+ # Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ import re
17
+ from typing import Any
18
+
19
+ from pydantic import Field
20
+ from typing_extensions import override
21
+
22
+ import veadk.config # noqa E401
23
+ from veadk.configs.database_configs import MilvusConfig
24
+ from veadk.configs.model_configs import EmbeddingModelConfig, NormalEmbeddingModelConfig
25
+ from veadk.knowledgebase.backends.base_backend import BaseKnowledgebaseBackend
26
+
27
+
28
+ class MilvusKnowledgeBackend(BaseKnowledgebaseBackend):
29
+ """Milvus-based backend for knowledgebase.
30
+
31
+ Milvus backend stores embedded chunks in a Milvus collection through
32
+ LlamaIndex's Milvus vector store. ``index`` maps directly to the Milvus
33
+ collection name.
34
+ """
35
+
36
+ milvus_config: MilvusConfig = Field(default_factory=MilvusConfig)
37
+ """Milvus connection config."""
38
+
39
+ embedding_config: EmbeddingModelConfig | NormalEmbeddingModelConfig = Field(
40
+ default_factory=EmbeddingModelConfig
41
+ )
42
+ """Embedding model configs."""
43
+
44
+ def model_post_init(self, __context: Any) -> None:
45
+ self.precheck_index_naming()
46
+ self._precheck_milvus_uri()
47
+
48
+ try:
49
+ from llama_index.core import StorageContext, VectorStoreIndex
50
+ from llama_index.vector_stores.milvus import MilvusVectorStore
51
+ from veadk.models.ark_embedding import create_embedding_model
52
+ except ImportError as e:
53
+ raise ImportError(
54
+ "Please install VeADK extensions\npip install veadk-python[extensions]"
55
+ ) from e
56
+
57
+ self._embed_model = create_embedding_model(
58
+ model_name=self.embedding_config.name,
59
+ api_key=self.embedding_config.api_key,
60
+ api_base=self.embedding_config.api_base,
61
+ )
62
+
63
+ vector_store_kwargs: dict[str, Any] = {
64
+ "uri": self.milvus_config.uri,
65
+ "collection_name": self.index,
66
+ "dim": self.embedding_config.dim,
67
+ "overwrite": self.milvus_config.overwrite,
68
+ }
69
+
70
+ token = self._resolve_token()
71
+ if token:
72
+ vector_store_kwargs["token"] = token
73
+
74
+ if self.milvus_config.db_name:
75
+ vector_store_kwargs["db_name"] = self.milvus_config.db_name
76
+
77
+ if self.milvus_config.timeout is not None:
78
+ vector_store_kwargs["timeout"] = self.milvus_config.timeout
79
+
80
+ output_fields = self._resolve_output_fields()
81
+ if output_fields:
82
+ vector_store_kwargs["output_fields"] = output_fields
83
+
84
+ self._vector_store = MilvusVectorStore(**vector_store_kwargs)
85
+ self._storage_context = StorageContext.from_defaults(
86
+ vector_store=self._vector_store
87
+ )
88
+ self._vector_index = VectorStoreIndex(
89
+ nodes=[],
90
+ storage_context=self._storage_context,
91
+ embed_model=self._embed_model,
92
+ )
93
+
94
+ @override
95
+ def precheck_index_naming(self) -> None:
96
+ if not isinstance(self.index, str) or not self.index:
97
+ raise ValueError("Milvus collection name must not be empty.")
98
+ if len(self.index) > 255:
99
+ raise ValueError("Milvus collection name is too long.")
100
+ if not re.fullmatch(r"^[A-Za-z_][A-Za-z0-9_]*$", self.index):
101
+ raise ValueError(
102
+ "Milvus collection name must start with a letter or underscore "
103
+ "and contain only letters, numbers, and underscores."
104
+ )
105
+
106
+ def _precheck_milvus_uri(self) -> None:
107
+ if not self.milvus_config.uri:
108
+ raise ValueError(
109
+ "Milvus uri must be configured via DATABASE_MILVUS_URI or "
110
+ "MilvusConfig(uri=...)."
111
+ )
112
+
113
+ @override
114
+ def add_from_directory(self, directory: str) -> bool:
115
+ from llama_index.core import SimpleDirectoryReader
116
+
117
+ documents = SimpleDirectoryReader(input_dir=directory).load_data()
118
+ nodes = self._split_documents(documents)
119
+ self._vector_index.insert_nodes(nodes)
120
+ return True
121
+
122
+ @override
123
+ def add_from_files(self, files: list[str]) -> bool:
124
+ from llama_index.core import SimpleDirectoryReader
125
+
126
+ documents = SimpleDirectoryReader(input_files=files).load_data()
127
+ nodes = self._split_documents(documents)
128
+ self._vector_index.insert_nodes(nodes)
129
+ return True
130
+
131
+ @override
132
+ def add_from_text(self, text: str | list[str]) -> bool:
133
+ from llama_index.core import Document
134
+
135
+ if isinstance(text, str):
136
+ documents = [Document(text=text)]
137
+ else:
138
+ documents = [Document(text=t) for t in text]
139
+ nodes = self._split_documents(documents)
140
+ self._vector_index.insert_nodes(nodes)
141
+ return True
142
+
143
+ @override
144
+ def search(self, query: str, top_k: int = 5) -> list[str]:
145
+ self._ensure_collection_loaded()
146
+ _retriever = self._vector_index.as_retriever(similarity_top_k=top_k)
147
+ retrieved_nodes = _retriever.retrieve(query)
148
+ return [node.text for node in retrieved_nodes]
149
+
150
+ def _resolve_token(self) -> str | None:
151
+ if self.milvus_config.token:
152
+ return self.milvus_config.token
153
+ if self.milvus_config.user and self.milvus_config.password:
154
+ return f"{self.milvus_config.user}:{self.milvus_config.password}"
155
+ return None
156
+
157
+ def _resolve_output_fields(self) -> list[str]:
158
+ output_fields = self.milvus_config.output_fields
159
+ if isinstance(output_fields, str):
160
+ output_fields = output_fields.strip()
161
+ if not output_fields:
162
+ return []
163
+ if output_fields.startswith("["):
164
+ parsed_output_fields = json.loads(output_fields)
165
+ if not isinstance(parsed_output_fields, list) or not all(
166
+ isinstance(field, str) for field in parsed_output_fields
167
+ ):
168
+ raise ValueError("Milvus output_fields must be a list of strings.")
169
+ output_fields = parsed_output_fields
170
+ else:
171
+ output_fields = output_fields.split(",")
172
+
173
+ return [field.strip() for field in output_fields if field.strip()]
174
+
175
+ def _ensure_collection_loaded(self) -> None:
176
+ vector_store = getattr(self, "_vector_store", None)
177
+ client = getattr(vector_store, "client", None)
178
+ collection_name = getattr(vector_store, "collection_name", self.index)
179
+ if client is None or not collection_name:
180
+ return
181
+
182
+ try:
183
+ load_state = client.get_load_state(collection_name=collection_name)
184
+ except Exception:
185
+ client.load_collection(collection_name=collection_name)
186
+ return
187
+
188
+ state = load_state.get("state") if isinstance(load_state, dict) else load_state
189
+ if "loaded" not in str(state).lower():
190
+ client.load_collection(collection_name=collection_name)
191
+
192
+ def _split_documents(self, documents: list[Any]) -> list[Any]:
193
+ """Split document into chunks."""
194
+ from veadk.knowledgebase.backends.utils import get_llama_index_splitter
195
+
196
+ nodes = []
197
+ for document in documents:
198
+ splitter = get_llama_index_splitter(document.metadata.get("file_path", ""))
199
+ _nodes = splitter.get_nodes_from_documents([document])
200
+ nodes.extend(_nodes)
201
+ return nodes