graph-dependency-analyzer 0.2.0__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.
- graph_dependency_analyzer-0.2.0.dist-info/METADATA +158 -0
- graph_dependency_analyzer-0.2.0.dist-info/RECORD +86 -0
- graph_dependency_analyzer-0.2.0.dist-info/WHEEL +5 -0
- graph_dependency_analyzer-0.2.0.dist-info/entry_points.txt +2 -0
- graph_dependency_analyzer-0.2.0.dist-info/top_level.txt +1 -0
- src/__init__.py +0 -0
- src/application/__init__.py +0 -0
- src/application/dtos/__init__.py +0 -0
- src/application/dtos/affected_component.py +59 -0
- src/application/dtos/batch_processing_result.py +17 -0
- src/application/dtos/impact_analysis_response.py +56 -0
- src/application/dtos/impact_query_request.py +38 -0
- src/application/dtos/index_batch_request.py +53 -0
- src/application/dtos/index_batch_response.py +72 -0
- src/application/dtos/index_result.py +20 -0
- src/application/dtos/repository_index_result.py +79 -0
- src/application/exceptions.py +41 -0
- src/application/services/__init__.py +0 -0
- src/application/services/file_scanner_service.py +258 -0
- src/application/services/progress_tracker.py +132 -0
- src/application/use_cases/__init__.py +0 -0
- src/application/use_cases/clear_all_data_use_case.py +180 -0
- src/application/use_cases/index_batch_use_case.py +153 -0
- src/application/use_cases/index_repository_use_case.py +683 -0
- src/application/use_cases/indexing_orchestrator.py +181 -0
- src/application/use_cases/list_repositories_use_case.py +84 -0
- src/cli.py +512 -0
- src/config/__init__.py +5 -0
- src/config/container.py +211 -0
- src/config/logging_config.py +123 -0
- src/config/settings.py +165 -0
- src/domain/__init__.py +0 -0
- src/domain/entities/__init__.py +20 -0
- src/domain/entities/ast_node.py +32 -0
- src/domain/entities/code_chunk.py +45 -0
- src/domain/entities/code_file.py +44 -0
- src/domain/entities/dependency_graph.py +110 -0
- src/domain/entities/repository.py +46 -0
- src/domain/exceptions.py +36 -0
- src/domain/repositories/__init__.py +22 -0
- src/domain/repositories/i_code_parser.py +50 -0
- src/domain/repositories/i_dependency_extractor.py +35 -0
- src/domain/repositories/i_embedding_provider.py +31 -0
- src/domain/repositories/i_graph_repository.py +78 -0
- src/domain/repositories/i_vector_repository.py +55 -0
- src/domain/services/__init__.py +8 -0
- src/domain/services/dependency_extractor.py +962 -0
- src/domain/services/semantic_chunk_builder.py +719 -0
- src/domain/value_objects/__init__.py +12 -0
- src/domain/value_objects/chunk_type.py +24 -0
- src/domain/value_objects/dependency_type.py +22 -0
- src/domain/value_objects/node_type.py +22 -0
- src/domain/value_objects/qualified_name.py +78 -0
- src/infrastructure/__init__.py +0 -0
- src/infrastructure/exceptions.py +41 -0
- src/infrastructure/external_apis/__init__.py +0 -0
- src/infrastructure/external_apis/ollama_embedding_provider.py +134 -0
- src/infrastructure/external_apis/openai_embedding_provider.py +231 -0
- src/infrastructure/parsing/__init__.py +20 -0
- src/infrastructure/parsing/config_file_parser.py +347 -0
- src/infrastructure/parsing/gradle_dependency_parser.py +159 -0
- src/infrastructure/parsing/groovy_parser.py +198 -0
- src/infrastructure/parsing/maven_dependency_parser.py +147 -0
- src/infrastructure/parsing/microfrontend_config_parser.py +242 -0
- src/infrastructure/parsing/npm_package_dependency_parser.py +255 -0
- src/infrastructure/parsing/python_parser.py +211 -0
- src/infrastructure/parsing/sql_schema_parser.py +658 -0
- src/infrastructure/parsing/tree_sitter_java_parser.py +434 -0
- src/infrastructure/parsing/tree_sitter_typescript_parser.py +600 -0
- src/infrastructure/persistence/__init__.py +0 -0
- src/infrastructure/persistence/graph_export_repository.py +443 -0
- src/infrastructure/persistence/neo4j_dependency_repository.py +657 -0
- src/infrastructure/persistence/qdrant_vector_repository.py +357 -0
- src/infrastructure/persistence/repository_relationship_repository.py +1312 -0
- src/presentation/__init__.py +0 -0
- src/presentation/api/__init__.py +0 -0
- src/presentation/api/rest/__init__.py +0 -0
- src/presentation/api/rest/exception_handlers.py +174 -0
- src/presentation/api/rest/main.py +80 -0
- src/presentation/api/rest/routers/__init__.py +5 -0
- src/presentation/api/rest/routers/admin.py +103 -0
- src/presentation/api/rest/routers/analysis.py +980 -0
- src/presentation/api/rest/routers/export.py +442 -0
- src/presentation/api/rest/routers/health.py +117 -0
- src/presentation/api/rest/routers/indexing.py +148 -0
- src/presentation/api/rest/routers/relationships.py +1089 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Domain value objects module."""
|
|
2
|
+
from src.domain.value_objects.qualified_name import QualifiedName
|
|
3
|
+
from src.domain.value_objects.dependency_type import DependencyType
|
|
4
|
+
from src.domain.value_objects.chunk_type import ChunkType
|
|
5
|
+
from src.domain.value_objects.node_type import NodeType
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"QualifiedName",
|
|
9
|
+
"DependencyType",
|
|
10
|
+
"ChunkType",
|
|
11
|
+
"NodeType",
|
|
12
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""ChunkType enum - types of code chunks for embeddings."""
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ChunkType(Enum):
|
|
6
|
+
"""
|
|
7
|
+
ChunkType enum representing types of semantic code chunks.
|
|
8
|
+
|
|
9
|
+
Values:
|
|
10
|
+
METHOD: Complete method with body
|
|
11
|
+
CLASS: Class declaration with imports (when no methods)
|
|
12
|
+
IMPORT_BLOCK: Import statements block
|
|
13
|
+
CONSTRUCTOR: Constructor method
|
|
14
|
+
FUNCTION: TypeScript/JavaScript function (Task 15.1)
|
|
15
|
+
HOOK: React hook function (Task 15.1)
|
|
16
|
+
COMPONENT: React/Angular/Vue component (Task 15.2)
|
|
17
|
+
"""
|
|
18
|
+
METHOD = "method"
|
|
19
|
+
CLASS = "class"
|
|
20
|
+
IMPORT_BLOCK = "import_block"
|
|
21
|
+
CONSTRUCTOR = "constructor"
|
|
22
|
+
FUNCTION = "function"
|
|
23
|
+
HOOK = "hook"
|
|
24
|
+
COMPONENT = "component"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""DependencyType enum - types of dependencies detected in code."""
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DependencyType(Enum):
|
|
6
|
+
"""
|
|
7
|
+
DependencyType enum representing types of dependencies.
|
|
8
|
+
|
|
9
|
+
Values:
|
|
10
|
+
LIBRARY: Maven/Gradle library dependency (e.g., org.springframework.boot:spring-boot-starter-web)
|
|
11
|
+
API: External HTTP/HTTPS API endpoint
|
|
12
|
+
DATABASE: Database connection (JDBC, JPA)
|
|
13
|
+
MICROSERVICE: Another internal microservice/repository
|
|
14
|
+
INTERNAL_CLASS: Class in the same repository
|
|
15
|
+
INTERNAL_METHOD: Method in the same repository
|
|
16
|
+
"""
|
|
17
|
+
LIBRARY = "library"
|
|
18
|
+
API = "api"
|
|
19
|
+
DATABASE = "database"
|
|
20
|
+
MICROSERVICE = "microservice"
|
|
21
|
+
INTERNAL_CLASS = "internal_class"
|
|
22
|
+
INTERNAL_METHOD = "internal_method"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""NodeType enum - types of AST nodes."""
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class NodeType(Enum):
|
|
6
|
+
"""
|
|
7
|
+
NodeType enum representing types of Abstract Syntax Tree nodes.
|
|
8
|
+
|
|
9
|
+
Values:
|
|
10
|
+
CLASS: Class declaration
|
|
11
|
+
INTERFACE: Interface declaration
|
|
12
|
+
METHOD: Method declaration
|
|
13
|
+
CONSTRUCTOR: Constructor declaration
|
|
14
|
+
IMPORT: Import statement
|
|
15
|
+
METHOD_CALL: Method invocation/call
|
|
16
|
+
"""
|
|
17
|
+
CLASS = "class"
|
|
18
|
+
INTERFACE = "interface"
|
|
19
|
+
METHOD = "method"
|
|
20
|
+
CONSTRUCTOR = "constructor"
|
|
21
|
+
IMPORT = "import"
|
|
22
|
+
METHOD_CALL = "method_call"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""QualifiedName value object - represents a fully qualified name."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class QualifiedName:
|
|
7
|
+
"""
|
|
8
|
+
QualifiedName value object representing a fully qualified name.
|
|
9
|
+
|
|
10
|
+
A qualified name follows the format: package.Class or package.Class.method
|
|
11
|
+
Examples:
|
|
12
|
+
- com.example.orders.OrderService
|
|
13
|
+
- com.example.orders.OrderService.createOrder
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
value: The fully qualified name string
|
|
17
|
+
|
|
18
|
+
Business Rules:
|
|
19
|
+
- Must contain at least one dot (package separator)
|
|
20
|
+
- Cannot be None or empty
|
|
21
|
+
- Immutable (frozen dataclass)
|
|
22
|
+
"""
|
|
23
|
+
value: str
|
|
24
|
+
|
|
25
|
+
def __post_init__(self):
|
|
26
|
+
"""Validate business rules after initialization."""
|
|
27
|
+
if self.value is None:
|
|
28
|
+
raise ValueError("Qualified name cannot be None or empty")
|
|
29
|
+
|
|
30
|
+
if not self.value or "." not in self.value:
|
|
31
|
+
raise ValueError("Qualified name must contain package")
|
|
32
|
+
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
"""Return string representation."""
|
|
35
|
+
return self.value
|
|
36
|
+
|
|
37
|
+
def get_package(self) -> str:
|
|
38
|
+
"""
|
|
39
|
+
Extract package from qualified name.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Package name (everything before the last class/method part)
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
com.example.orders.OrderService -> com.example.orders
|
|
46
|
+
com.example.orders.OrderService.createOrder -> com.example.orders
|
|
47
|
+
"""
|
|
48
|
+
parts = self.value.split(".")
|
|
49
|
+
|
|
50
|
+
# If it's package.Class.method, package is everything except last 2 parts
|
|
51
|
+
# If it's package.Class, package is everything except last part
|
|
52
|
+
# Heuristic: if last part starts with lowercase, it's a method
|
|
53
|
+
if len(parts) >= 3 and parts[-1][0].islower():
|
|
54
|
+
# package.Class.method -> package
|
|
55
|
+
return ".".join(parts[:-2])
|
|
56
|
+
else:
|
|
57
|
+
# package.Class -> package
|
|
58
|
+
return ".".join(parts[:-1])
|
|
59
|
+
|
|
60
|
+
def get_class_name(self) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Extract class name from qualified name.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Class name (simple name without package)
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
com.example.orders.OrderService -> OrderService
|
|
69
|
+
com.example.orders.OrderService.createOrder -> OrderService
|
|
70
|
+
"""
|
|
71
|
+
parts = self.value.split(".")
|
|
72
|
+
|
|
73
|
+
# If last part starts with lowercase, it's a method, so class is second-to-last
|
|
74
|
+
if len(parts) >= 2 and parts[-1][0].islower():
|
|
75
|
+
return parts[-2]
|
|
76
|
+
else:
|
|
77
|
+
# Last part is the class
|
|
78
|
+
return parts[-1]
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Infrastructure Layer Exceptions (Task 9.3)
|
|
3
|
+
|
|
4
|
+
Exception hierarchy for infrastructure layer errors.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InfrastructureException(Exception):
|
|
9
|
+
"""Base exception for infrastructure layer errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, details: dict | None = None):
|
|
12
|
+
"""
|
|
13
|
+
Initialize infrastructure exception.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
message: Human-readable error message
|
|
17
|
+
details: Optional dict with additional context
|
|
18
|
+
"""
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
self.message = message
|
|
21
|
+
self.details = details or {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OpenAIAPIError(InfrastructureException):
|
|
25
|
+
"""Error communicating with OpenAI API."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class QdrantConnectionError(InfrastructureException):
|
|
30
|
+
"""Error connecting to or communicating with Qdrant."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Neo4jConnectionError(InfrastructureException):
|
|
35
|
+
"""Error connecting to or communicating with Neo4j."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ParseError(InfrastructureException):
|
|
40
|
+
"""Error parsing source code."""
|
|
41
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ollama Embedding Provider — generates embeddings using a local Ollama instance.
|
|
3
|
+
|
|
4
|
+
No API keys, no rate limits, fully local.
|
|
5
|
+
|
|
6
|
+
Supported models (install with `ollama pull <model>`):
|
|
7
|
+
nomic-embed-text — 768-dim, fast, good quality (recommended)
|
|
8
|
+
mxbai-embed-large — 1024-dim, best quality, slower
|
|
9
|
+
all-minilm — 384-dim, very fast, lower quality
|
|
10
|
+
|
|
11
|
+
Set in .env:
|
|
12
|
+
EMBEDDING_PROVIDER=ollama
|
|
13
|
+
OLLAMA_BASE_URL=http://localhost:11434
|
|
14
|
+
OLLAMA_EMBEDDING_MODEL=nomic-embed-text
|
|
15
|
+
OLLAMA_EMBEDDING_DIM=768
|
|
16
|
+
QDRANT_VECTOR_SIZE=768 ← must match the model dimension
|
|
17
|
+
|
|
18
|
+
Install Ollama: https://ollama.com/download
|
|
19
|
+
Pull a model: ollama pull nomic-embed-text
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import structlog
|
|
24
|
+
import httpx
|
|
25
|
+
|
|
26
|
+
logger = structlog.get_logger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OllamaEmbeddingProvider:
|
|
30
|
+
"""
|
|
31
|
+
Local embedding provider using Ollama.
|
|
32
|
+
|
|
33
|
+
Supports batch processing (sequentially — Ollama processes one at a time).
|
|
34
|
+
No rate limits, no API key required.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, config=None):
|
|
38
|
+
base_url = "http://localhost:11434"
|
|
39
|
+
model = "nomic-embed-text"
|
|
40
|
+
|
|
41
|
+
if config is not None:
|
|
42
|
+
base_url = getattr(config, "ollama_base_url", base_url)
|
|
43
|
+
model = getattr(config, "ollama_embedding_model", model)
|
|
44
|
+
|
|
45
|
+
self.base_url = base_url.rstrip("/")
|
|
46
|
+
self.model = model
|
|
47
|
+
self._client: httpx.AsyncClient | None = None
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def client(self) -> httpx.AsyncClient:
|
|
51
|
+
if self._client is None:
|
|
52
|
+
self._client = httpx.AsyncClient(
|
|
53
|
+
timeout=120.0,
|
|
54
|
+
limits=httpx.Limits(max_connections=4)
|
|
55
|
+
)
|
|
56
|
+
return self._client
|
|
57
|
+
|
|
58
|
+
async def embed_batch(
|
|
59
|
+
self,
|
|
60
|
+
texts: list[str],
|
|
61
|
+
batch_size: int = 10,
|
|
62
|
+
) -> list[list[float]]:
|
|
63
|
+
"""
|
|
64
|
+
Generate embeddings for a list of texts using Ollama.
|
|
65
|
+
|
|
66
|
+
Ollama's /api/embed endpoint supports multiple inputs in one call.
|
|
67
|
+
We process in batches to avoid memory issues with large lists.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
texts: List of text strings to embed
|
|
71
|
+
batch_size: Number of texts per Ollama API call (default 10)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of embedding vectors
|
|
75
|
+
"""
|
|
76
|
+
if not texts:
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
all_embeddings: list[list[float]] = []
|
|
80
|
+
|
|
81
|
+
for i in range(0, len(texts), batch_size):
|
|
82
|
+
batch = texts[i:i + batch_size]
|
|
83
|
+
embeddings = await self._embed_batch_ollama(batch)
|
|
84
|
+
all_embeddings.extend(embeddings)
|
|
85
|
+
|
|
86
|
+
logger.info(
|
|
87
|
+
"embeddings_generated",
|
|
88
|
+
model=self.model,
|
|
89
|
+
total_texts=len(texts),
|
|
90
|
+
total_embeddings=len(all_embeddings),
|
|
91
|
+
provider="ollama",
|
|
92
|
+
)
|
|
93
|
+
return all_embeddings
|
|
94
|
+
|
|
95
|
+
async def _embed_batch_ollama(self, texts: list[str]) -> list[list[float]]:
|
|
96
|
+
"""Call Ollama /api/embed for a batch of texts."""
|
|
97
|
+
url = f"{self.base_url}/api/embed"
|
|
98
|
+
|
|
99
|
+
payload = {
|
|
100
|
+
"model": self.model,
|
|
101
|
+
"input": texts,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
response = await self.client.post(url, json=payload)
|
|
106
|
+
response.raise_for_status()
|
|
107
|
+
data = response.json()
|
|
108
|
+
|
|
109
|
+
# Ollama /api/embed returns {"embeddings": [[...], [...]]}
|
|
110
|
+
embeddings = data.get("embeddings", [])
|
|
111
|
+
|
|
112
|
+
if len(embeddings) != len(texts):
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Ollama returned {len(embeddings)} embeddings for {len(texts)} texts"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return embeddings
|
|
118
|
+
|
|
119
|
+
except httpx.ConnectError:
|
|
120
|
+
raise ConnectionError(
|
|
121
|
+
f"Cannot connect to Ollama at {self.base_url}. "
|
|
122
|
+
"Make sure Ollama is running: `ollama serve`"
|
|
123
|
+
)
|
|
124
|
+
except httpx.HTTPStatusError as e:
|
|
125
|
+
if e.response.status_code == 404:
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"Model '{self.model}' not found in Ollama. "
|
|
128
|
+
f"Pull it first: `ollama pull {self.model}`"
|
|
129
|
+
)
|
|
130
|
+
raise
|
|
131
|
+
|
|
132
|
+
async def close(self):
|
|
133
|
+
if self._client is not None:
|
|
134
|
+
await self._client.aclose()
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI Embedding Provider Implementation (Task 6.1)
|
|
3
|
+
|
|
4
|
+
Generates embeddings via OpenAI API with support for Flow AI proxy.
|
|
5
|
+
Implements retry logic with exponential backoff for resilience.
|
|
6
|
+
"""
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
import structlog
|
|
10
|
+
import httpx
|
|
11
|
+
from tenacity import (
|
|
12
|
+
retry,
|
|
13
|
+
stop_after_attempt,
|
|
14
|
+
wait_exponential,
|
|
15
|
+
retry_if_exception_type
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from src.config.settings import EmbeddingConfig, get_settings
|
|
19
|
+
|
|
20
|
+
logger = structlog.get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EmbeddingProviderError(Exception):
|
|
24
|
+
"""Error raised when embedding generation fails"""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OpenAIEmbeddingProvider:
|
|
29
|
+
"""
|
|
30
|
+
OpenAI API client for generating embeddings.
|
|
31
|
+
|
|
32
|
+
Supports:
|
|
33
|
+
- OpenAI standard endpoint
|
|
34
|
+
- Flow AI (CI&T proxy) with custom headers
|
|
35
|
+
- Local embedding servers
|
|
36
|
+
- Batch processing (10 texts per API call)
|
|
37
|
+
- Automatic retry with exponential backoff (10 attempts, 2-30s)
|
|
38
|
+
- 1536-dimensional embeddings (text-embedding-3-small)
|
|
39
|
+
|
|
40
|
+
Configuration via .env:
|
|
41
|
+
- OPENAI_API_KEY: API key (required)
|
|
42
|
+
- OPENAI_BASE_URL: Base URL (default: https://api.openai.com/v1)
|
|
43
|
+
- OPENAI_EMBEDDING_MODEL: Model name (default: text-embedding-3-small)
|
|
44
|
+
|
|
45
|
+
Requirements: 8.1-8.7
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
config: EmbeddingConfig | None = None,
|
|
51
|
+
client: httpx.AsyncClient | None = None
|
|
52
|
+
):
|
|
53
|
+
"""
|
|
54
|
+
Initialize embedding provider.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
config: Embedding configuration (default: loaded from .env)
|
|
58
|
+
client: Optional httpx client (for testing)
|
|
59
|
+
"""
|
|
60
|
+
if config is None:
|
|
61
|
+
config = get_settings().embedding
|
|
62
|
+
|
|
63
|
+
self.api_key = config.openai_api_key
|
|
64
|
+
self.base_url = config.openai_base_url.rstrip("/")
|
|
65
|
+
self.model = config.openai_embedding_model
|
|
66
|
+
self._client = client
|
|
67
|
+
self._is_flow_ai = "flow.ciandt.com" in self.base_url
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def client(self) -> httpx.AsyncClient:
|
|
71
|
+
"""Get or create httpx client"""
|
|
72
|
+
if self._client is None:
|
|
73
|
+
# Desabilitar verificação SSL para ambientes corporativos
|
|
74
|
+
self._client = httpx.AsyncClient(
|
|
75
|
+
timeout=180.0,
|
|
76
|
+
verify=False, # Desabilita verificação SSL
|
|
77
|
+
limits=httpx.Limits(max_connections=10)
|
|
78
|
+
)
|
|
79
|
+
logger.debug("httpx_client_created", verify_ssl=False)
|
|
80
|
+
return self._client
|
|
81
|
+
|
|
82
|
+
async def embed_batch(
|
|
83
|
+
self,
|
|
84
|
+
texts: list[str],
|
|
85
|
+
batch_size: int = 10
|
|
86
|
+
) -> list[list[float]]:
|
|
87
|
+
"""
|
|
88
|
+
Generate embeddings for batch of texts.
|
|
89
|
+
|
|
90
|
+
Processes texts in batches for optimal API usage.
|
|
91
|
+
Automatically retries on failures with exponential backoff.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
texts: List of text chunks to embed
|
|
95
|
+
batch_size: Number of texts per API call (default 10)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
List of embedding vectors (1536 floats each)
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
EmbeddingProviderError: If API fails after retries
|
|
102
|
+
"""
|
|
103
|
+
all_embeddings = []
|
|
104
|
+
|
|
105
|
+
# Process in batches
|
|
106
|
+
for i in range(0, len(texts), batch_size):
|
|
107
|
+
batch_texts = texts[i:i + batch_size]
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
# Call API for this batch with retry
|
|
111
|
+
batch_embeddings = await self._generate_embeddings_with_retry(batch_texts)
|
|
112
|
+
all_embeddings.extend(batch_embeddings)
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.error(
|
|
116
|
+
"embedding_generation_failed",
|
|
117
|
+
error=str(e),
|
|
118
|
+
error_type=type(e).__name__
|
|
119
|
+
)
|
|
120
|
+
raise EmbeddingProviderError(f"Failed to generate embeddings: {e}")
|
|
121
|
+
|
|
122
|
+
logger.info(
|
|
123
|
+
"embeddings_generated",
|
|
124
|
+
total_texts=len(texts),
|
|
125
|
+
total_embeddings=len(all_embeddings),
|
|
126
|
+
model=self.model
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return all_embeddings
|
|
130
|
+
|
|
131
|
+
@retry(
|
|
132
|
+
stop=stop_after_attempt(10),
|
|
133
|
+
wait=wait_exponential(min=2, max=30),
|
|
134
|
+
retry=retry_if_exception_type(httpx.HTTPError)
|
|
135
|
+
)
|
|
136
|
+
async def _generate_embeddings_with_retry(self, texts: list[str]) -> list[list[float]]:
|
|
137
|
+
"""
|
|
138
|
+
Generate embeddings with retry logic.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
texts: List of texts to embed
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
List of embedding vectors
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
httpx.HTTPError: On API failures (for retry)
|
|
148
|
+
"""
|
|
149
|
+
return await self._generate_embeddings(texts)
|
|
150
|
+
|
|
151
|
+
async def _generate_embeddings(self, texts: list[str]) -> list[list[float]]:
|
|
152
|
+
"""
|
|
153
|
+
Generate embeddings for a single batch via API call.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
texts: List of texts (max 10)
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
List of embedding vectors
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
httpx.HTTPError: On API failures
|
|
163
|
+
EmbeddingProviderError: On validation failures
|
|
164
|
+
"""
|
|
165
|
+
# Build headers
|
|
166
|
+
headers = {
|
|
167
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
168
|
+
"Content-Type": "application/json"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Add Flow AI custom header if needed
|
|
172
|
+
if self._is_flow_ai:
|
|
173
|
+
headers["flowAgent"] = "knowledge-graph-indexing"
|
|
174
|
+
|
|
175
|
+
# Build payload
|
|
176
|
+
payload = {
|
|
177
|
+
"model": self.model,
|
|
178
|
+
"input": texts,
|
|
179
|
+
"encoding_format": "float"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# POST to API - endpoint depends on API type
|
|
183
|
+
if self._is_flow_ai:
|
|
184
|
+
# Flow AI requires /v1/openai/embeddings path
|
|
185
|
+
url = f"{self.base_url}/v1/openai/embeddings"
|
|
186
|
+
else:
|
|
187
|
+
# OpenAI standard endpoint
|
|
188
|
+
url = f"{self.base_url}/embeddings"
|
|
189
|
+
|
|
190
|
+
logger.info(
|
|
191
|
+
"embedding_request_starting",
|
|
192
|
+
url=url,
|
|
193
|
+
is_flow_ai=self._is_flow_ai,
|
|
194
|
+
texts_count=len(texts),
|
|
195
|
+
model=self.model
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
response = await self.client.post(
|
|
199
|
+
url=url,
|
|
200
|
+
headers=headers,
|
|
201
|
+
json=payload
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Validate response
|
|
205
|
+
response.raise_for_status() # Let httpx.HTTPError propagate for retry
|
|
206
|
+
|
|
207
|
+
# Parse response
|
|
208
|
+
data = response.json()
|
|
209
|
+
|
|
210
|
+
# Extract embeddings
|
|
211
|
+
if "data" not in data:
|
|
212
|
+
raise EmbeddingProviderError("API response missing 'data' field")
|
|
213
|
+
|
|
214
|
+
embeddings_data = data["data"]
|
|
215
|
+
|
|
216
|
+
# Validate count
|
|
217
|
+
if len(embeddings_data) != len(texts):
|
|
218
|
+
raise EmbeddingProviderError(
|
|
219
|
+
f"Embedding count mismatch: expected {len(texts)}, "
|
|
220
|
+
f"got {len(embeddings_data)}"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Extract embedding vectors
|
|
224
|
+
embeddings = [item["embedding"] for item in embeddings_data]
|
|
225
|
+
|
|
226
|
+
return embeddings
|
|
227
|
+
|
|
228
|
+
async def close(self):
|
|
229
|
+
"""Close the HTTP client"""
|
|
230
|
+
if self._client is not None:
|
|
231
|
+
await self._client.aclose()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Infrastructure parsing module."""
|
|
2
|
+
from src.infrastructure.parsing.tree_sitter_java_parser import TreeSitterJavaParser, ParseError
|
|
3
|
+
from src.infrastructure.parsing.tree_sitter_typescript_parser import TreeSitterTypeScriptParser
|
|
4
|
+
from src.infrastructure.parsing.maven_dependency_parser import MavenDependencyParser
|
|
5
|
+
from src.infrastructure.parsing.gradle_dependency_parser import GradleDependencyParser
|
|
6
|
+
from src.infrastructure.parsing.config_file_parser import ConfigFileParser, ParsedConfig
|
|
7
|
+
from src.infrastructure.parsing.npm_package_dependency_parser import NpmPackageDependencyParser
|
|
8
|
+
from src.infrastructure.parsing.microfrontend_config_parser import MicrofrontendConfigParser
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"TreeSitterJavaParser",
|
|
12
|
+
"TreeSitterTypeScriptParser",
|
|
13
|
+
"ParseError",
|
|
14
|
+
"MavenDependencyParser",
|
|
15
|
+
"GradleDependencyParser",
|
|
16
|
+
"ConfigFileParser",
|
|
17
|
+
"ParsedConfig",
|
|
18
|
+
"NpmPackageDependencyParser",
|
|
19
|
+
"MicrofrontendConfigParser",
|
|
20
|
+
]
|