chaskiwasi 0.2.0__tar.gz
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.
- chaskiwasi-0.2.0/.github/workflows/ci.yml +31 -0
- chaskiwasi-0.2.0/.github/workflows/publish.yml +35 -0
- chaskiwasi-0.2.0/.gitignore +142 -0
- chaskiwasi-0.2.0/PKG-INFO +31 -0
- chaskiwasi-0.2.0/README.md +1 -0
- chaskiwasi-0.2.0/pyproject.toml +47 -0
- chaskiwasi-0.2.0/src/__init__.py +0 -0
- chaskiwasi-0.2.0/src/chaskiwasi/__init__.py +0 -0
- chaskiwasi-0.2.0/src/chaskiwasi/chunking/__init__.py +0 -0
- chaskiwasi-0.2.0/src/chaskiwasi/chunking/chunk_tokenizer.py +43 -0
- chaskiwasi-0.2.0/src/chaskiwasi/classification/__init__.py +0 -0
- chaskiwasi-0.2.0/src/chaskiwasi/classification/cascade_factory.py +68 -0
- chaskiwasi-0.2.0/src/chaskiwasi/classification/strategies/__init__.py +0 -0
- chaskiwasi-0.2.0/src/chaskiwasi/classification/strategies/base_strategy.py +43 -0
- chaskiwasi-0.2.0/src/chaskiwasi/classification/strategies/context_overlap_strategy.py +21 -0
- chaskiwasi-0.2.0/src/chaskiwasi/classification/strategies/cpu_regex_strategy.py +62 -0
- chaskiwasi-0.2.0/src/chaskiwasi/classification/strategies/gemini_streamer.py +99 -0
- chaskiwasi-0.2.0/src/chaskiwasi/classification/strategies/llm_router_strategy.py +71 -0
- chaskiwasi-0.2.0/src/chaskiwasi/config/__init__.py +0 -0
- chaskiwasi-0.2.0/src/chaskiwasi/config/settings.py +19 -0
- chaskiwasi-0.2.0/src/chaskiwasi/config/taxonomy_registry.py +55 -0
- chaskiwasi-0.2.0/src/chaskiwasi/consolidation/consolidator.py +158 -0
- chaskiwasi-0.2.0/src/chaskiwasi/ingestion/__init__.py +0 -0
- chaskiwasi-0.2.0/src/chaskiwasi/ingestion/docling_parser.py +64 -0
- chaskiwasi-0.2.0/src/chaskiwasi/query_engine/__init__.py +0 -0
- chaskiwasi-0.2.0/src/chaskiwasi/query_engine/cross_filter.py +74 -0
- chaskiwasi-0.2.0/src/chaskiwasi/query_engine/ollama_streamer.py +64 -0
- chaskiwasi-0.2.0/src/chaskiwasi/storage/__init__.py +0 -0
- chaskiwasi-0.2.0/src/chaskiwasi/storage/chroma_persistent.py +67 -0
- chaskiwasi-0.2.0/tests/conftest.py +42 -0
- chaskiwasi-0.2.0/tests/test_cascade_factory.py +134 -0
- chaskiwasi-0.2.0/tests/test_chroma_persistent.py +101 -0
- chaskiwasi-0.2.0/tests/test_chunk_tokenizer.py +99 -0
- chaskiwasi-0.2.0/tests/test_consolidation.py +168 -0
- chaskiwasi-0.2.0/tests/test_consolidator_batch.py +73 -0
- chaskiwasi-0.2.0/tests/test_core.py +2 -0
- chaskiwasi-0.2.0/tests/test_cross_filter.py +83 -0
- chaskiwasi-0.2.0/tests/test_docling_parser.py +86 -0
- chaskiwasi-0.2.0/tests/test_gemini_streamer.py +121 -0
- chaskiwasi-0.2.0/tests/test_llm_router_strategy.py +115 -0
- chaskiwasi-0.2.0/tests/test_ollama_streamer.py +159 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: CI (Tests)
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ "main", "master" ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ "main", "master" ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.10"
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install --upgrade pip
|
|
27
|
+
pip install pytest
|
|
28
|
+
pip install .
|
|
29
|
+
- name: Run tests
|
|
30
|
+
run: |
|
|
31
|
+
pytest
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch: # Permite activarlo manualmente desde la interfaz de GitHub
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write # Requerido obligatoriamente para Trusted Publishers (OIDC)
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
pypi-publish:
|
|
13
|
+
name: Upload release to PyPI
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment:
|
|
16
|
+
name: pypi
|
|
17
|
+
url: https://pypi.org/p/chaskiwasi # Reemplaza con el nombre real de tu paquete en PyPI
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.10"
|
|
25
|
+
|
|
26
|
+
- name: Install build dependencies
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade pip
|
|
29
|
+
pip install build
|
|
30
|
+
|
|
31
|
+
- name: Build package
|
|
32
|
+
run: python -m build
|
|
33
|
+
|
|
34
|
+
- name: Publish package to PyPI
|
|
35
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# Installer logs
|
|
30
|
+
pip-log.txt
|
|
31
|
+
pip-delete-this-directory.txt
|
|
32
|
+
|
|
33
|
+
# Unit test / coverage reports
|
|
34
|
+
htmlcov/
|
|
35
|
+
.tox/
|
|
36
|
+
.nox/
|
|
37
|
+
.coverage
|
|
38
|
+
.coverage.*
|
|
39
|
+
.cache
|
|
40
|
+
nosetests.xml
|
|
41
|
+
coverage.xml
|
|
42
|
+
*.cover
|
|
43
|
+
*.py,cover
|
|
44
|
+
.hypothesis/
|
|
45
|
+
.pytest_cache/
|
|
46
|
+
cover/
|
|
47
|
+
|
|
48
|
+
# Translations
|
|
49
|
+
*.mo
|
|
50
|
+
*.pot
|
|
51
|
+
|
|
52
|
+
# Django stuff
|
|
53
|
+
*.log
|
|
54
|
+
local_settings.py
|
|
55
|
+
db.sqlite3
|
|
56
|
+
db.sqlite3-journal
|
|
57
|
+
|
|
58
|
+
# Flask stuff
|
|
59
|
+
instance/
|
|
60
|
+
.webassets-cache
|
|
61
|
+
|
|
62
|
+
# Scrapy stuff
|
|
63
|
+
.scrapy
|
|
64
|
+
|
|
65
|
+
# Sphinx documentation
|
|
66
|
+
docs/_build/
|
|
67
|
+
|
|
68
|
+
# PyBuilder
|
|
69
|
+
.pybuilder/
|
|
70
|
+
target/
|
|
71
|
+
|
|
72
|
+
# Jupyter Notebook
|
|
73
|
+
.ipynb_checkpoints
|
|
74
|
+
|
|
75
|
+
# IPython
|
|
76
|
+
profile_default/
|
|
77
|
+
ipython_config.py
|
|
78
|
+
|
|
79
|
+
# pyenv
|
|
80
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
81
|
+
# intended to run in multiple environments; otherwise, let them be committed:
|
|
82
|
+
.python-version
|
|
83
|
+
|
|
84
|
+
# Pipenv
|
|
85
|
+
#Rely on Pipenv to maintain the version lockfile, but commit locked dependencies:
|
|
86
|
+
#Pipfile.lock
|
|
87
|
+
|
|
88
|
+
# Poetry
|
|
89
|
+
#Rely on Poetry to maintain the version lockfile, but commit locked dependencies:
|
|
90
|
+
#poetry.lock
|
|
91
|
+
|
|
92
|
+
# pdm
|
|
93
|
+
#Rely on PDM to maintain the version lockfile, but commit locked dependencies:
|
|
94
|
+
#pdm.lock
|
|
95
|
+
|
|
96
|
+
# PEP 582; used by e.g. __pypackages__
|
|
97
|
+
__pypackages__/
|
|
98
|
+
|
|
99
|
+
# Celery stuff
|
|
100
|
+
celerybeat-schedule
|
|
101
|
+
celerybeat.pid
|
|
102
|
+
|
|
103
|
+
# SageMath parsed files
|
|
104
|
+
*.sage.py
|
|
105
|
+
|
|
106
|
+
# Environments
|
|
107
|
+
.env
|
|
108
|
+
.venv
|
|
109
|
+
env/
|
|
110
|
+
venv/
|
|
111
|
+
ENV/
|
|
112
|
+
env.bak/
|
|
113
|
+
venv.bak/
|
|
114
|
+
|
|
115
|
+
# Spyder project settings
|
|
116
|
+
.spyderproject
|
|
117
|
+
.spyproject
|
|
118
|
+
|
|
119
|
+
# Rope project settings
|
|
120
|
+
.ropeproject
|
|
121
|
+
|
|
122
|
+
# mypy
|
|
123
|
+
.mypy_cache/
|
|
124
|
+
.dmypy.json
|
|
125
|
+
.dmypy.root
|
|
126
|
+
|
|
127
|
+
# Pyre type checker
|
|
128
|
+
.pyre/
|
|
129
|
+
|
|
130
|
+
# pytype static type analyzer
|
|
131
|
+
.pytype/
|
|
132
|
+
|
|
133
|
+
# Cython debug symbols
|
|
134
|
+
cython_debug/
|
|
135
|
+
|
|
136
|
+
# IDEs and editors
|
|
137
|
+
.idea/
|
|
138
|
+
.vscode/
|
|
139
|
+
*.swp
|
|
140
|
+
*.swo
|
|
141
|
+
.DS_Store
|
|
142
|
+
Thumbs.db
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: chaskiwasi
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Sistema de consulta semántica para fuentes jurídicas.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Maorda/chaskywasi
|
|
6
|
+
Project-URL: Issues, https://github.com/Maorda/chaskywasi/issues
|
|
7
|
+
Author-email: Luis Maurtua <luis.maurtua@ejemplo.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: chromadb>=0.5.0
|
|
17
|
+
Requires-Dist: docling>=2.0.0
|
|
18
|
+
Requires-Dist: google-genai>=0.1.0
|
|
19
|
+
Requires-Dist: httpx>=0.27.0
|
|
20
|
+
Requires-Dist: ijson>=3.3.0
|
|
21
|
+
Requires-Dist: requests>=2.32.0
|
|
22
|
+
Requires-Dist: streamlit>=1.30.0
|
|
23
|
+
Requires-Dist: tiktoken>=0.7.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: anyio>=4.0.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: faker>=20.0.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
GOLA
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
GOLA
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.18.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chaskiwasi" # <-- Cambiado al nombre real de tu librería
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Sistema de consulta semántica para fuentes jurídicas."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Luis Maurtua", email = "luis.maurtua@ejemplo.com" }
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
]
|
|
22
|
+
requires-python = ">=3.10"
|
|
23
|
+
dependencies = [
|
|
24
|
+
"docling>=2.0.0",
|
|
25
|
+
"tiktoken>=0.7.0",
|
|
26
|
+
"chromadb>=0.5.0",
|
|
27
|
+
"ijson>=3.3.0",
|
|
28
|
+
"requests>=2.32.0",
|
|
29
|
+
"streamlit>=1.30.0",
|
|
30
|
+
"google-genai>=0.1.0",
|
|
31
|
+
"httpx>=0.27.0"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8.0.0",
|
|
37
|
+
"pytest-asyncio>=0.23.0",
|
|
38
|
+
"anyio>=4.0.0",
|
|
39
|
+
"faker>=20.0.0"
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/Maorda/chaskywasi"
|
|
44
|
+
Issues = "https://github.com/Maorda/chaskywasi/issues"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["src/chaskiwasi"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# dantesito/chasky/chunking/chunk_tokenizer.py
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
import tiktoken
|
|
6
|
+
|
|
7
|
+
from chaskiwasi.config.settings import Settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ChunkTokenizer:
|
|
11
|
+
"""Divide textos en fragmentos mediante ventanas de tokens de tiktoken."""
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
self._encoding = tiktoken.get_encoding(Settings.TIKTOKEN_ENCODING)
|
|
15
|
+
|
|
16
|
+
def split_text(self, text: str) -> List[str]:
|
|
17
|
+
"""Divide el texto en chunks determinados exclusivamente por tokens."""
|
|
18
|
+
if not isinstance(text, str) or not text:
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
tokens: List[int] = self._encoding.encode(text)
|
|
22
|
+
|
|
23
|
+
if not tokens:
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
chunk_size: int = Settings.CHUNK_SIZE
|
|
27
|
+
step: int = Settings.CHUNK_SIZE - Settings.CHUNK_OVERLAP
|
|
28
|
+
|
|
29
|
+
chunks: List[str] = []
|
|
30
|
+
start: int = 0
|
|
31
|
+
total_tokens: int = len(tokens)
|
|
32
|
+
|
|
33
|
+
while start < total_tokens:
|
|
34
|
+
end: int = min(start + chunk_size, total_tokens)
|
|
35
|
+
chunk_tokens: List[int] = tokens[start:end]
|
|
36
|
+
chunks.append(self._encoding.decode(chunk_tokens))
|
|
37
|
+
|
|
38
|
+
if end >= total_tokens:
|
|
39
|
+
break
|
|
40
|
+
|
|
41
|
+
start += step
|
|
42
|
+
|
|
43
|
+
return chunks
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, List, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from chaskiwasi.classification.strategies.base_strategy import BaseStrategy
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
class CascadeFactory:
|
|
9
|
+
"""
|
|
10
|
+
Fábrica y orquestador de clasificación en cascada agnóstico para chaskywasi.
|
|
11
|
+
Aplica una arquitectura de enrutamiento por estrategias ordenadas jerárquicamente.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
strategies: Optional[List[BaseStrategy]] = None,
|
|
17
|
+
default_source: Optional[Any] = None,
|
|
18
|
+
default_section: Optional[Any] = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
self._strategies: List[BaseStrategy] = strategies or []
|
|
21
|
+
self.default_source = default_source
|
|
22
|
+
self.default_section = default_section
|
|
23
|
+
|
|
24
|
+
def process_chunk(
|
|
25
|
+
self,
|
|
26
|
+
text: Optional[str],
|
|
27
|
+
current_source: Optional[Any] = None,
|
|
28
|
+
) -> Tuple[Optional[Any], Optional[Any]]:
|
|
29
|
+
"""Procesa un fragmento individual de texto iterando sobre la lista de estrategias."""
|
|
30
|
+
effective_default_source = current_source or self.default_source
|
|
31
|
+
|
|
32
|
+
if not text or not text.strip():
|
|
33
|
+
return effective_default_source, self.default_section
|
|
34
|
+
|
|
35
|
+
for strategy in self._strategies:
|
|
36
|
+
try:
|
|
37
|
+
src_res, sec_res = strategy.classify(
|
|
38
|
+
text, current_source=effective_default_source
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if sec_res is not None:
|
|
42
|
+
final_source = src_res or effective_default_source
|
|
43
|
+
return final_source, sec_res
|
|
44
|
+
except Exception as e:
|
|
45
|
+
logger.warning(
|
|
46
|
+
"Estrategia %s falló durante la clasificación: %s",
|
|
47
|
+
strategy.__class__.__name__,
|
|
48
|
+
str(e),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return effective_default_source, self.default_section
|
|
52
|
+
|
|
53
|
+
def process_chunks_batch(
|
|
54
|
+
self,
|
|
55
|
+
chunks: List[str],
|
|
56
|
+
current_source: Optional[Any] = None,
|
|
57
|
+
) -> List[Tuple[Optional[Any], Optional[Any]]]:
|
|
58
|
+
"""Procesa una lista de fragmentos secuencialmente manteniendo el contexto actual."""
|
|
59
|
+
results = []
|
|
60
|
+
active_source = current_source or self.default_source
|
|
61
|
+
|
|
62
|
+
for chunk in chunks:
|
|
63
|
+
src, sec = self.process_chunk(chunk, current_source=active_source)
|
|
64
|
+
if src is not None:
|
|
65
|
+
active_source = src
|
|
66
|
+
results.append((src, sec))
|
|
67
|
+
|
|
68
|
+
return results
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# dantesito/chasky/classification/strategies/base_strategy.py
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Tuple, Optional, Any
|
|
4
|
+
|
|
5
|
+
# 🚀 CONTRATO ACTIVO: Importación obligatoria para control de tipos dinámicos
|
|
6
|
+
from chaskiwasi.config.taxonomy_registry import TaxonomyRegistry
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseStrategy(ABC):
|
|
10
|
+
"""
|
|
11
|
+
Interfaz contractual agnóstica para todas las estrategias de inferencia semántica.
|
|
12
|
+
|
|
13
|
+
Expone de forma segura las clases de Enums inyectadas por el cliente mediante
|
|
14
|
+
propiedades dinámicas de solo lectura, eliminando la fragilidad de constructores.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def source_enum_class(self) -> Any:
|
|
19
|
+
"""
|
|
20
|
+
Devuelve de forma dinámica la clase SourceEnum del cliente.
|
|
21
|
+
🛡️ CORTAFUEGOS EN CALIENTE: Lanza RuntimeError si no fue inyectada al arrancar.
|
|
22
|
+
"""
|
|
23
|
+
return TaxonomyRegistry.get_source_enum()
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def section_enum_class(self) -> Any:
|
|
27
|
+
"""
|
|
28
|
+
Devuelve de forma dinámica la clase SectionEnum del cliente.
|
|
29
|
+
🛡️ CORTAFUEGOS EN CALIENTE: Lanza RuntimeError si no fue inyectada al arrancar.
|
|
30
|
+
"""
|
|
31
|
+
return TaxonomyRegistry.get_section_enum()
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def classify(self, chunk: str, current_source: Optional[str] = None) -> Tuple[Optional[Any], Optional[Any]]:
|
|
35
|
+
"""
|
|
36
|
+
Analiza un fragmento y devuelve una tupla conteniendo las instancias
|
|
37
|
+
de los Enums dinámicos del cliente (SourceEnum, SectionEnum).
|
|
38
|
+
|
|
39
|
+
:param chunk: Fragmento de texto Markdown a evaluar.
|
|
40
|
+
:param current_source: Contexto de origen acumulado o arrastrado.
|
|
41
|
+
:return: Tupla conteniendo (Instancia de SourceEnum, Instancia de SectionEnum)
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# dantesito/chasky/classification/strategies/context_overlap_strategy.py
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from chaskiwasi.classification.strategies.base_strategy import BaseStrategy
|
|
6
|
+
from chaskiwasi.config.taxonomy_registry import SectionEnum, SourceEnum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ContextOverlapStrategy(BaseStrategy):
|
|
10
|
+
"""Arrastra la fuente institucional desde el contexto del chunk anterior."""
|
|
11
|
+
|
|
12
|
+
def classify(
|
|
13
|
+
self,
|
|
14
|
+
chunk_text: str,
|
|
15
|
+
current_source: Optional[SourceEnum] = None,
|
|
16
|
+
) -> Tuple[Optional[SourceEnum], Optional[SectionEnum]]:
|
|
17
|
+
"""Mantiene la fuente previa sin inspeccionar el contenido del chunk."""
|
|
18
|
+
if current_source is not None:
|
|
19
|
+
return current_source, None
|
|
20
|
+
|
|
21
|
+
return None, None
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any, List, Optional, Pattern, Tuple
|
|
4
|
+
from chaskiwasi.classification.strategies.base_strategy import BaseStrategy
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class RegexRule:
|
|
9
|
+
"""Estructura de datos para definir reglas de clasificación por Regex."""
|
|
10
|
+
source: Any
|
|
11
|
+
section: Any
|
|
12
|
+
patterns: List[str]
|
|
13
|
+
flags: int = re.IGNORECASE | re.DOTALL
|
|
14
|
+
compiled_patterns: List[Pattern[str]] = field(init=False, repr=False)
|
|
15
|
+
|
|
16
|
+
def __post_init__(self) -> None:
|
|
17
|
+
compiled: List[Pattern[str]] = []
|
|
18
|
+
for pattern in self.patterns:
|
|
19
|
+
try:
|
|
20
|
+
compiled.append(re.compile(pattern, self.flags))
|
|
21
|
+
except re.error as err:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
f"Patrón Regex inválido '{pattern}' para la regla ({self.source}, {self.section}): {err}"
|
|
24
|
+
) from err
|
|
25
|
+
self.compiled_patterns = compiled
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CPURegexStrategy(BaseStrategy):
|
|
29
|
+
"""Estrategia genérica que ejecuta reglas Regex inyectadas."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, rules: Optional[List[RegexRule]] = None) -> None:
|
|
32
|
+
self.rules: List[RegexRule] = list(rules) if rules else []
|
|
33
|
+
|
|
34
|
+
def add_rule(self, rule: RegexRule) -> None:
|
|
35
|
+
"""Permite registrar reglas dinámicamente."""
|
|
36
|
+
self.rules.append(rule)
|
|
37
|
+
|
|
38
|
+
def classify(
|
|
39
|
+
self,
|
|
40
|
+
chunk_text: str,
|
|
41
|
+
current_source: Optional[Any] = None,
|
|
42
|
+
) -> Tuple[Optional[Any], Optional[Any]]:
|
|
43
|
+
if not isinstance(chunk_text, str) or not chunk_text:
|
|
44
|
+
return None, None
|
|
45
|
+
|
|
46
|
+
# Evaluación con preferencia contextual por current_source si aplica
|
|
47
|
+
for rule in self.rules:
|
|
48
|
+
if current_source and rule.source != current_source:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
if any(pattern.search(chunk_text) for pattern in rule.compiled_patterns):
|
|
52
|
+
return rule.source, rule.section
|
|
53
|
+
|
|
54
|
+
# Segunda pasada para reglas generales si no coincidió en el contexto actual
|
|
55
|
+
if current_source:
|
|
56
|
+
for rule in self.rules:
|
|
57
|
+
if rule.source == current_source:
|
|
58
|
+
continue
|
|
59
|
+
if any(pattern.search(chunk_text) for pattern in rule.compiled_patterns):
|
|
60
|
+
return rule.source, rule.section
|
|
61
|
+
|
|
62
|
+
return None, None
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Dict, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from google import genai
|
|
6
|
+
from google.genai import types
|
|
7
|
+
|
|
8
|
+
from chaskiwasi.classification.strategies.base_strategy import BaseStrategy
|
|
9
|
+
|
|
10
|
+
logging.getLogger("google.genai").setLevel(logging.ERROR)
|
|
11
|
+
logging.getLogger("google.genai._api_client").setLevel(logging.ERROR)
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GeminiStreamer(BaseStrategy):
|
|
17
|
+
"""Estrategia de inferencia remota en la nube totalmente agnóstica al dominio."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
system_instruction: str,
|
|
22
|
+
label_mapping: Dict[str, Any],
|
|
23
|
+
) -> None:
|
|
24
|
+
self.system_instruction = system_instruction
|
|
25
|
+
self.label_mapping = label_mapping
|
|
26
|
+
|
|
27
|
+
api_key = ""
|
|
28
|
+
env_key = os.environ.get("GEMINI_API_KEY", "").strip()
|
|
29
|
+
if env_key:
|
|
30
|
+
api_key = env_key
|
|
31
|
+
|
|
32
|
+
if not api_key:
|
|
33
|
+
env_path = ".env"
|
|
34
|
+
if os.path.exists(env_path):
|
|
35
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
36
|
+
for line in f:
|
|
37
|
+
clean_line = line.strip()
|
|
38
|
+
if clean_line.startswith("GEMINI_API_KEY"):
|
|
39
|
+
try:
|
|
40
|
+
parsed_tokens = clean_line.split("=", 1)
|
|
41
|
+
if len(parsed_tokens) == 2:
|
|
42
|
+
parsed_key = parsed_tokens[1].strip().strip('"').strip("'")
|
|
43
|
+
if parsed_key:
|
|
44
|
+
api_key = parsed_key
|
|
45
|
+
break
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
if not api_key or not (api_key.startswith("AIzaSy") or api_key.startswith("AQ.")):
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"🚨 ERROR CRÍTICO DE CONFIGURACIÓN: La variable 'GEMINI_API_KEY' "
|
|
52
|
+
"no está definida en el entorno ni en el archivo .env raíz."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
self.client = genai.Client(api_key=api_key)
|
|
56
|
+
self.model_name = "gemini-3.5-flash-lite"
|
|
57
|
+
|
|
58
|
+
def classify(
|
|
59
|
+
self, chunk_text: str, current_source: Optional[Any] = None
|
|
60
|
+
) -> Tuple[Optional[Any], Optional[Any]]:
|
|
61
|
+
if not chunk_text or not chunk_text.strip():
|
|
62
|
+
return None, None
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
response = self.client.models.generate_content(
|
|
66
|
+
model=self.model_name,
|
|
67
|
+
contents=chunk_text,
|
|
68
|
+
config=types.GenerateContentConfig(
|
|
69
|
+
system_instruction=self.system_instruction,
|
|
70
|
+
temperature=0.1,
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not response or not hasattr(response, "text") or not response.text:
|
|
75
|
+
logger.warning("Respuesta vacía o no válida recibida de la API de Gemini.")
|
|
76
|
+
return None, None
|
|
77
|
+
|
|
78
|
+
label = response.text.strip().lower()
|
|
79
|
+
|
|
80
|
+
if label == "desconocido":
|
|
81
|
+
return None, None
|
|
82
|
+
|
|
83
|
+
section_enum = self.label_mapping.get(label)
|
|
84
|
+
if section_enum is None:
|
|
85
|
+
logger.warning(
|
|
86
|
+
"Etiqueta desconocida o fuera de taxonomía devuelta por Gemini: '%s'", label
|
|
87
|
+
)
|
|
88
|
+
return None, None
|
|
89
|
+
|
|
90
|
+
# Asumimos que Gemini por ahora solo devuelve la sección, manteniendo la firma original
|
|
91
|
+
return None, section_enum
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(
|
|
95
|
+
"Error durante la inferencia remota con el cliente Google GenAI: %s",
|
|
96
|
+
str(e),
|
|
97
|
+
exc_info=True,
|
|
98
|
+
)
|
|
99
|
+
return None, None
|