alita-sdk 0.3.257__py3-none-any.whl → 0.3.562__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.
- alita_sdk/cli/__init__.py +10 -0
- alita_sdk/cli/__main__.py +17 -0
- alita_sdk/cli/agent/__init__.py +5 -0
- alita_sdk/cli/agent/default.py +258 -0
- alita_sdk/cli/agent_executor.py +155 -0
- alita_sdk/cli/agent_loader.py +215 -0
- alita_sdk/cli/agent_ui.py +228 -0
- alita_sdk/cli/agents.py +3601 -0
- alita_sdk/cli/callbacks.py +647 -0
- alita_sdk/cli/cli.py +168 -0
- alita_sdk/cli/config.py +306 -0
- alita_sdk/cli/context/__init__.py +30 -0
- alita_sdk/cli/context/cleanup.py +198 -0
- alita_sdk/cli/context/manager.py +731 -0
- alita_sdk/cli/context/message.py +285 -0
- alita_sdk/cli/context/strategies.py +289 -0
- alita_sdk/cli/context/token_estimation.py +127 -0
- alita_sdk/cli/formatting.py +182 -0
- alita_sdk/cli/input_handler.py +419 -0
- alita_sdk/cli/inventory.py +1073 -0
- alita_sdk/cli/mcp_loader.py +315 -0
- alita_sdk/cli/toolkit.py +327 -0
- alita_sdk/cli/toolkit_loader.py +85 -0
- alita_sdk/cli/tools/__init__.py +43 -0
- alita_sdk/cli/tools/approval.py +224 -0
- alita_sdk/cli/tools/filesystem.py +1751 -0
- alita_sdk/cli/tools/planning.py +389 -0
- alita_sdk/cli/tools/terminal.py +414 -0
- alita_sdk/community/__init__.py +72 -12
- alita_sdk/community/inventory/__init__.py +236 -0
- alita_sdk/community/inventory/config.py +257 -0
- alita_sdk/community/inventory/enrichment.py +2137 -0
- alita_sdk/community/inventory/extractors.py +1469 -0
- alita_sdk/community/inventory/ingestion.py +3172 -0
- alita_sdk/community/inventory/knowledge_graph.py +1457 -0
- alita_sdk/community/inventory/parsers/__init__.py +218 -0
- alita_sdk/community/inventory/parsers/base.py +295 -0
- alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
- alita_sdk/community/inventory/parsers/go_parser.py +851 -0
- alita_sdk/community/inventory/parsers/html_parser.py +389 -0
- alita_sdk/community/inventory/parsers/java_parser.py +593 -0
- alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
- alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
- alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
- alita_sdk/community/inventory/parsers/python_parser.py +604 -0
- alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
- alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
- alita_sdk/community/inventory/parsers/text_parser.py +322 -0
- alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
- alita_sdk/community/inventory/patterns/__init__.py +61 -0
- alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
- alita_sdk/community/inventory/patterns/loader.py +348 -0
- alita_sdk/community/inventory/patterns/registry.py +198 -0
- alita_sdk/community/inventory/presets.py +535 -0
- alita_sdk/community/inventory/retrieval.py +1403 -0
- alita_sdk/community/inventory/toolkit.py +173 -0
- alita_sdk/community/inventory/toolkit_utils.py +176 -0
- alita_sdk/community/inventory/visualize.py +1370 -0
- alita_sdk/configurations/__init__.py +11 -0
- alita_sdk/configurations/ado.py +148 -2
- alita_sdk/configurations/azure_search.py +1 -1
- alita_sdk/configurations/bigquery.py +1 -1
- alita_sdk/configurations/bitbucket.py +94 -2
- alita_sdk/configurations/browser.py +18 -0
- alita_sdk/configurations/carrier.py +19 -0
- alita_sdk/configurations/confluence.py +130 -1
- alita_sdk/configurations/delta_lake.py +1 -1
- alita_sdk/configurations/figma.py +76 -5
- alita_sdk/configurations/github.py +65 -1
- alita_sdk/configurations/gitlab.py +81 -0
- alita_sdk/configurations/google_places.py +17 -0
- alita_sdk/configurations/jira.py +103 -0
- alita_sdk/configurations/openapi.py +111 -0
- alita_sdk/configurations/postman.py +1 -1
- alita_sdk/configurations/qtest.py +72 -3
- alita_sdk/configurations/report_portal.py +115 -0
- alita_sdk/configurations/salesforce.py +19 -0
- alita_sdk/configurations/service_now.py +1 -12
- alita_sdk/configurations/sharepoint.py +167 -0
- alita_sdk/configurations/sonar.py +18 -0
- alita_sdk/configurations/sql.py +20 -0
- alita_sdk/configurations/testio.py +101 -0
- alita_sdk/configurations/testrail.py +88 -0
- alita_sdk/configurations/xray.py +94 -1
- alita_sdk/configurations/zephyr_enterprise.py +94 -1
- alita_sdk/configurations/zephyr_essential.py +95 -0
- alita_sdk/runtime/clients/artifact.py +21 -4
- alita_sdk/runtime/clients/client.py +458 -67
- alita_sdk/runtime/clients/mcp_discovery.py +342 -0
- alita_sdk/runtime/clients/mcp_manager.py +262 -0
- alita_sdk/runtime/clients/sandbox_client.py +352 -0
- alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
- alita_sdk/runtime/langchain/assistant.py +183 -43
- alita_sdk/runtime/langchain/constants.py +647 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +209 -31
- alita_sdk/runtime/langchain/document_loaders/AlitaImageLoader.py +1 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaMarkdownLoader.py +66 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaPDFLoader.py +79 -10
- alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +52 -15
- alita_sdk/runtime/langchain/document_loaders/AlitaPythonLoader.py +9 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaTableLoader.py +1 -4
- alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +15 -2
- alita_sdk/runtime/langchain/document_loaders/ImageParser.py +30 -0
- alita_sdk/runtime/langchain/document_loaders/constants.py +189 -41
- alita_sdk/runtime/langchain/interfaces/llm_processor.py +4 -2
- alita_sdk/runtime/langchain/langraph_agent.py +407 -92
- alita_sdk/runtime/langchain/utils.py +102 -8
- alita_sdk/runtime/llms/preloaded.py +2 -6
- alita_sdk/runtime/models/mcp_models.py +61 -0
- alita_sdk/runtime/skills/__init__.py +91 -0
- alita_sdk/runtime/skills/callbacks.py +498 -0
- alita_sdk/runtime/skills/discovery.py +540 -0
- alita_sdk/runtime/skills/executor.py +610 -0
- alita_sdk/runtime/skills/input_builder.py +371 -0
- alita_sdk/runtime/skills/models.py +330 -0
- alita_sdk/runtime/skills/registry.py +355 -0
- alita_sdk/runtime/skills/skill_runner.py +330 -0
- alita_sdk/runtime/toolkits/__init__.py +28 -0
- alita_sdk/runtime/toolkits/application.py +14 -4
- alita_sdk/runtime/toolkits/artifact.py +24 -9
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +780 -0
- alita_sdk/runtime/toolkits/planning.py +178 -0
- alita_sdk/runtime/toolkits/skill_router.py +238 -0
- alita_sdk/runtime/toolkits/subgraph.py +11 -6
- alita_sdk/runtime/toolkits/tools.py +314 -70
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/__init__.py +24 -0
- alita_sdk/runtime/tools/application.py +16 -4
- alita_sdk/runtime/tools/artifact.py +367 -33
- alita_sdk/runtime/tools/data_analysis.py +183 -0
- alita_sdk/runtime/tools/function.py +100 -4
- alita_sdk/runtime/tools/graph.py +81 -0
- alita_sdk/runtime/tools/image_generation.py +218 -0
- alita_sdk/runtime/tools/llm.py +1013 -177
- alita_sdk/runtime/tools/loop.py +3 -1
- alita_sdk/runtime/tools/loop_output.py +3 -1
- alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
- alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
- alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
- alita_sdk/runtime/tools/planning/__init__.py +36 -0
- alita_sdk/runtime/tools/planning/models.py +246 -0
- alita_sdk/runtime/tools/planning/wrapper.py +607 -0
- alita_sdk/runtime/tools/router.py +2 -1
- alita_sdk/runtime/tools/sandbox.py +375 -0
- alita_sdk/runtime/tools/skill_router.py +776 -0
- alita_sdk/runtime/tools/tool.py +3 -1
- alita_sdk/runtime/tools/vectorstore.py +69 -65
- alita_sdk/runtime/tools/vectorstore_base.py +163 -90
- alita_sdk/runtime/utils/AlitaCallback.py +137 -21
- alita_sdk/runtime/utils/mcp_client.py +492 -0
- alita_sdk/runtime/utils/mcp_oauth.py +361 -0
- alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
- alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
- alita_sdk/runtime/utils/streamlit.py +41 -14
- alita_sdk/runtime/utils/toolkit_utils.py +28 -9
- alita_sdk/runtime/utils/utils.py +48 -0
- alita_sdk/tools/__init__.py +135 -37
- alita_sdk/tools/ado/__init__.py +2 -2
- alita_sdk/tools/ado/repos/__init__.py +15 -19
- alita_sdk/tools/ado/repos/repos_wrapper.py +12 -20
- alita_sdk/tools/ado/test_plan/__init__.py +26 -8
- alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +56 -28
- alita_sdk/tools/ado/wiki/__init__.py +27 -12
- alita_sdk/tools/ado/wiki/ado_wrapper.py +114 -40
- alita_sdk/tools/ado/work_item/__init__.py +27 -12
- alita_sdk/tools/ado/work_item/ado_wrapper.py +95 -11
- alita_sdk/tools/advanced_jira_mining/__init__.py +12 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +14 -11
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +13 -8
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +454 -110
- alita_sdk/tools/bitbucket/__init__.py +27 -19
- alita_sdk/tools/bitbucket/api_wrapper.py +285 -27
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +5 -5
- alita_sdk/tools/browser/__init__.py +41 -16
- alita_sdk/tools/browser/crawler.py +3 -1
- alita_sdk/tools/browser/utils.py +15 -6
- alita_sdk/tools/carrier/__init__.py +18 -17
- alita_sdk/tools/carrier/backend_reports_tool.py +8 -4
- alita_sdk/tools/carrier/excel_reporter.py +8 -4
- alita_sdk/tools/chunkers/__init__.py +3 -1
- alita_sdk/tools/chunkers/code/codeparser.py +1 -1
- alita_sdk/tools/chunkers/sematic/json_chunker.py +2 -1
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
- alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
- alita_sdk/tools/chunkers/universal_chunker.py +270 -0
- alita_sdk/tools/cloud/aws/__init__.py +11 -7
- alita_sdk/tools/cloud/azure/__init__.py +11 -7
- alita_sdk/tools/cloud/gcp/__init__.py +11 -7
- alita_sdk/tools/cloud/k8s/__init__.py +11 -7
- alita_sdk/tools/code/linter/__init__.py +9 -8
- alita_sdk/tools/code/loaders/codesearcher.py +3 -2
- alita_sdk/tools/code/sonar/__init__.py +20 -13
- alita_sdk/tools/code_indexer_toolkit.py +199 -0
- alita_sdk/tools/confluence/__init__.py +21 -14
- alita_sdk/tools/confluence/api_wrapper.py +197 -58
- alita_sdk/tools/confluence/loader.py +14 -2
- alita_sdk/tools/custom_open_api/__init__.py +11 -5
- alita_sdk/tools/elastic/__init__.py +10 -8
- alita_sdk/tools/elitea_base.py +546 -64
- alita_sdk/tools/figma/__init__.py +11 -8
- alita_sdk/tools/figma/api_wrapper.py +352 -153
- alita_sdk/tools/github/__init__.py +17 -17
- alita_sdk/tools/github/api_wrapper.py +9 -26
- alita_sdk/tools/github/github_client.py +81 -12
- alita_sdk/tools/github/schemas.py +2 -1
- alita_sdk/tools/github/tool.py +5 -1
- alita_sdk/tools/gitlab/__init__.py +18 -13
- alita_sdk/tools/gitlab/api_wrapper.py +224 -80
- alita_sdk/tools/gitlab_org/__init__.py +13 -10
- alita_sdk/tools/google/bigquery/__init__.py +13 -13
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +20 -11
- alita_sdk/tools/jira/__init__.py +21 -11
- alita_sdk/tools/jira/api_wrapper.py +315 -168
- alita_sdk/tools/keycloak/__init__.py +10 -8
- alita_sdk/tools/localgit/__init__.py +8 -3
- alita_sdk/tools/localgit/local_git.py +62 -54
- alita_sdk/tools/localgit/tool.py +5 -1
- alita_sdk/tools/memory/__init__.py +38 -14
- alita_sdk/tools/non_code_indexer_toolkit.py +7 -2
- alita_sdk/tools/ocr/__init__.py +10 -8
- alita_sdk/tools/openapi/__init__.py +281 -108
- alita_sdk/tools/openapi/api_wrapper.py +883 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +18 -11
- alita_sdk/tools/pandas/api_wrapper.py +40 -45
- alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
- alita_sdk/tools/postman/__init__.py +10 -11
- alita_sdk/tools/postman/api_wrapper.py +19 -8
- alita_sdk/tools/postman/postman_analysis.py +8 -1
- alita_sdk/tools/pptx/__init__.py +10 -10
- alita_sdk/tools/qtest/__init__.py +21 -14
- alita_sdk/tools/qtest/api_wrapper.py +1784 -88
- alita_sdk/tools/rally/__init__.py +12 -10
- alita_sdk/tools/report_portal/__init__.py +22 -16
- alita_sdk/tools/salesforce/__init__.py +21 -16
- alita_sdk/tools/servicenow/__init__.py +20 -16
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +16 -14
- alita_sdk/tools/sharepoint/api_wrapper.py +179 -39
- alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
- alita_sdk/tools/sharepoint/utils.py +8 -2
- alita_sdk/tools/slack/__init__.py +11 -7
- alita_sdk/tools/sql/__init__.py +21 -19
- alita_sdk/tools/sql/api_wrapper.py +71 -23
- alita_sdk/tools/testio/__init__.py +20 -13
- alita_sdk/tools/testrail/__init__.py +12 -11
- alita_sdk/tools/testrail/api_wrapper.py +214 -46
- alita_sdk/tools/utils/__init__.py +28 -4
- alita_sdk/tools/utils/content_parser.py +182 -62
- alita_sdk/tools/utils/text_operations.py +254 -0
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +83 -27
- alita_sdk/tools/xray/__init__.py +17 -14
- alita_sdk/tools/xray/api_wrapper.py +58 -113
- alita_sdk/tools/yagmail/__init__.py +8 -3
- alita_sdk/tools/zephyr/__init__.py +11 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +15 -9
- alita_sdk/tools/zephyr_enterprise/api_wrapper.py +30 -15
- alita_sdk/tools/zephyr_essential/__init__.py +15 -10
- alita_sdk/tools/zephyr_essential/api_wrapper.py +297 -54
- alita_sdk/tools/zephyr_essential/client.py +6 -4
- alita_sdk/tools/zephyr_scale/__init__.py +12 -8
- alita_sdk/tools/zephyr_scale/api_wrapper.py +39 -31
- alita_sdk/tools/zephyr_squad/__init__.py +11 -7
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/METADATA +184 -37
- alita_sdk-0.3.562.dist-info/RECORD +450 -0
- alita_sdk-0.3.562.dist-info/entry_points.txt +2 -0
- alita_sdk/tools/bitbucket/tools.py +0 -304
- alita_sdk-0.3.257.dist-info/RECORD +0 -343
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ import re
|
|
|
4
4
|
import traceback
|
|
5
5
|
from json import JSONDecodeError
|
|
6
6
|
from traceback import format_exc
|
|
7
|
-
from typing import List, Optional, Any, Dict, Generator
|
|
7
|
+
from typing import List, Optional, Any, Dict, Generator, Literal
|
|
8
8
|
import os
|
|
9
9
|
|
|
10
10
|
from atlassian import Jira
|
|
@@ -13,10 +13,11 @@ from langchain_core.tools import ToolException
|
|
|
13
13
|
from pydantic import Field, PrivateAttr, model_validator, create_model, SecretStr
|
|
14
14
|
import requests
|
|
15
15
|
|
|
16
|
-
from ..elitea_base import BaseVectorStoreToolApiWrapper, extend_with_vector_tools
|
|
17
16
|
from ..llm.img_utils import ImageDescriptionCache
|
|
17
|
+
from ..non_code_indexer_toolkit import NonCodeIndexerToolkit
|
|
18
18
|
from ..utils import is_cookie_token, parse_cookie_string
|
|
19
|
-
from ..utils.
|
|
19
|
+
from ..utils.available_tools_decorator import extend_with_parent_available_tools
|
|
20
|
+
from ..utils.content_parser import file_extension_by_chunker, process_content_by_type
|
|
20
21
|
from ...runtime.utils.utils import IndexerKeywords
|
|
21
22
|
|
|
22
23
|
logger = logging.getLogger(__name__)
|
|
@@ -131,6 +132,13 @@ GetRemoteLinks = create_model(
|
|
|
131
132
|
jira_issue_key=(str, Field(description="Jira issue key from which remote links will be extracted, e.g. TEST-1234"))
|
|
132
133
|
)
|
|
133
134
|
|
|
135
|
+
GetIssueAttachments = create_model(
|
|
136
|
+
"GetIssueAttachments",
|
|
137
|
+
jira_issue_key=(str, Field(description="Jira issue key from which remote links will be extracted, e.g. TEST-1234")),
|
|
138
|
+
attachment_pattern=(Optional[str], Field(description="Regex pattern to filter attachment filenames. If not provided,"
|
|
139
|
+
" all attachments will be processed", default=None))
|
|
140
|
+
)
|
|
141
|
+
|
|
134
142
|
ListCommentsInput = create_model(
|
|
135
143
|
"ListCommentsInputModel",
|
|
136
144
|
issue_key=(str, Field(description="The issue key of the Jira issue from which comments will be extracted, e.g. 'TEST-123'."))
|
|
@@ -391,7 +399,7 @@ def process_search_response(jira_url, response, payload_params: Dict[str, Any] =
|
|
|
391
399
|
|
|
392
400
|
return str(processed_issues)
|
|
393
401
|
|
|
394
|
-
class JiraApiWrapper(
|
|
402
|
+
class JiraApiWrapper(NonCodeIndexerToolkit):
|
|
395
403
|
base_url: str
|
|
396
404
|
api_version: Optional[str] = "2",
|
|
397
405
|
api_key: Optional[SecretStr] = None,
|
|
@@ -436,50 +444,72 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
436
444
|
cls._client = Jira(url=url, token=token, cloud=cloud, verify_ssl=values['verify_ssl'], api_version=api_version)
|
|
437
445
|
else:
|
|
438
446
|
cls._client = Jira(url=url, username=username, password=api_key, cloud=cloud, verify_ssl=values['verify_ssl'], api_version=api_version)
|
|
439
|
-
custom_headers = values.get('custom_headers'
|
|
447
|
+
custom_headers = values.get('custom_headers') or {}
|
|
440
448
|
logger.info(f"Jira tool: custom headers length: {len(custom_headers)}")
|
|
441
449
|
for header, value in custom_headers.items():
|
|
442
450
|
cls._client._update_header(header, value)
|
|
443
451
|
|
|
444
452
|
cls.llm=values.get('llm')
|
|
445
|
-
return values
|
|
453
|
+
return super().validate_toolkit(values)
|
|
446
454
|
|
|
447
455
|
def _parse_issues(self, issues: Dict) -> List[dict]:
|
|
448
|
-
parsed = []
|
|
449
|
-
|
|
450
|
-
|
|
456
|
+
parsed: List[dict] = []
|
|
457
|
+
issues_list = issues.get("issues") if isinstance(issues, dict) else None
|
|
458
|
+
if not isinstance(issues_list, list):
|
|
459
|
+
return parsed
|
|
460
|
+
|
|
461
|
+
for issue in issues_list:
|
|
462
|
+
if self.limit and len(parsed) >= self.limit:
|
|
451
463
|
break
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
464
|
+
|
|
465
|
+
issue_fields = issue.get("fields") or {}
|
|
466
|
+
key = issue.get("key", "")
|
|
467
|
+
issue_id = issue.get("id", "")
|
|
468
|
+
|
|
469
|
+
summary = issue_fields.get("summary") or ""
|
|
470
|
+
description = issue_fields.get("description") or ""
|
|
471
|
+
created_raw = issue_fields.get("created") or ""
|
|
472
|
+
created = created_raw[:10] if created_raw else ""
|
|
473
|
+
updated = issue_fields.get("updated") or ""
|
|
474
|
+
duedate = issue_fields.get("duedate")
|
|
475
|
+
|
|
476
|
+
priority_info = issue_fields.get("priority") or {}
|
|
477
|
+
priority = priority_info.get("name") or "None"
|
|
478
|
+
|
|
479
|
+
status_info = issue_fields.get("status") or {}
|
|
480
|
+
status = status_info.get("name") or "Unknown"
|
|
481
|
+
|
|
482
|
+
project_info = issue_fields.get("project") or {}
|
|
483
|
+
project_id = project_info.get("id") or ""
|
|
484
|
+
|
|
485
|
+
issue_url = f"{self._client.url}browse/{key}" if key else self._client.url
|
|
486
|
+
|
|
487
|
+
assignee_info = issue_fields.get("assignee") or {}
|
|
488
|
+
assignee = assignee_info.get("displayName") or "None"
|
|
489
|
+
|
|
468
490
|
rel_issues = {}
|
|
469
|
-
for related_issue in issue_fields
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
491
|
+
for related_issue in issue_fields.get("issuelinks") or []:
|
|
492
|
+
rel_type = None
|
|
493
|
+
rel_key = None
|
|
494
|
+
if related_issue.get("inwardIssue"):
|
|
495
|
+
rel_type = related_issue.get("type", {}).get("inward")
|
|
496
|
+
rel_key = related_issue["inwardIssue"].get("key")
|
|
473
497
|
# rel_summary = related_issue["inwardIssue"]["fields"]["summary"]
|
|
474
|
-
|
|
475
|
-
rel_type = related_issue
|
|
476
|
-
rel_key = related_issue["outwardIssue"]
|
|
498
|
+
elif related_issue.get("outwardIssue"):
|
|
499
|
+
rel_type = related_issue.get("type", {}).get("outward")
|
|
500
|
+
rel_key = related_issue["outwardIssue"].get("key")
|
|
477
501
|
# rel_summary = related_issue["outwardIssue"]["fields"]["summary"]
|
|
478
|
-
|
|
502
|
+
|
|
503
|
+
if rel_type and rel_key:
|
|
504
|
+
rel_issues = {
|
|
505
|
+
"type": rel_type,
|
|
506
|
+
"key": rel_key,
|
|
507
|
+
"url": f"{self._client.url}browse/{rel_key}",
|
|
508
|
+
}
|
|
479
509
|
|
|
480
510
|
parsed_issue = {
|
|
481
511
|
"key": key,
|
|
482
|
-
"id":
|
|
512
|
+
"id": issue_id,
|
|
483
513
|
"projectId": project_id,
|
|
484
514
|
"summary": summary,
|
|
485
515
|
"description": description,
|
|
@@ -492,10 +522,13 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
492
522
|
"url": issue_url,
|
|
493
523
|
"related_issues": rel_issues,
|
|
494
524
|
}
|
|
495
|
-
|
|
496
|
-
|
|
525
|
+
|
|
526
|
+
for field in (self.additional_fields or []):
|
|
527
|
+
field_value = issue_fields.get(field)
|
|
497
528
|
parsed_issue[field] = field_value
|
|
529
|
+
|
|
498
530
|
parsed.append(parsed_issue)
|
|
531
|
+
|
|
499
532
|
return parsed
|
|
500
533
|
|
|
501
534
|
@staticmethod
|
|
@@ -555,12 +588,14 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
555
588
|
Use the appropriate issue link type (e.g., "Test", "Relates", "Blocks").
|
|
556
589
|
If we use "Test" linktype, the test is inward issue, the story/other issue is outward issue.."""
|
|
557
590
|
|
|
591
|
+
comment = f"Issue {inward_issue_key} was linked to {outward_issue_key}."
|
|
592
|
+
comment_body = {"content": [{"content": [{"text": comment,"type": "text"}],"type": "paragraph"}],"type": "doc","version": 1} if self.api_version == "3" else comment
|
|
558
593
|
link_data = {
|
|
559
594
|
"type": {"name": f"{linktype}"},
|
|
560
595
|
"inwardIssue": {"key": f"{inward_issue_key}"},
|
|
561
596
|
"outwardIssue": {"key": f"{outward_issue_key}"},
|
|
562
597
|
"comment": {
|
|
563
|
-
"body":
|
|
598
|
+
"body": comment_body
|
|
564
599
|
}
|
|
565
600
|
}
|
|
566
601
|
self._client.create_issue_link(link_data)
|
|
@@ -698,6 +733,8 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
698
733
|
def add_comments(self, issue_key: str, comment: str):
|
|
699
734
|
""" Add a comment to a Jira issue."""
|
|
700
735
|
try:
|
|
736
|
+
if self.api_version == '3':
|
|
737
|
+
comment = {"content": [{"content": [{"text": comment,"type": "text"}],"type": "paragraph"}],"type": "doc","version": 1}
|
|
701
738
|
self._client.issue_add_comment(issue_key, comment)
|
|
702
739
|
issue_url = f"{self._client.url}browse/{issue_key}"
|
|
703
740
|
output = f"Done. Comment is added for issue {issue_key}. You can view it at {issue_url}"
|
|
@@ -721,22 +758,48 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
721
758
|
return parsed_projects_str
|
|
722
759
|
except Exception:
|
|
723
760
|
stacktrace = format_exc()
|
|
724
|
-
logger.error(f"Error
|
|
725
|
-
return ToolException(f"Error
|
|
726
|
-
|
|
727
|
-
def get_attachments_content(self, jira_issue_key: str):
|
|
728
|
-
""" Extract content of all attachments related to specified Jira issue key.
|
|
729
|
-
NOTE: only parsable attachments will be considered
|
|
761
|
+
logger.error(f"Error listing Jira projects: {stacktrace}")
|
|
762
|
+
return ToolException(f"Error listing Jira projects: {stacktrace}")
|
|
763
|
+
|
|
764
|
+
def get_attachments_content(self, jira_issue_key: str, attachment_pattern: Optional[str] = None):
|
|
765
|
+
""" Extract the content of all attachments related to a specified Jira issue key.
|
|
766
|
+
NOTE: only parsable attachments will be considered
|
|
767
|
+
Args:
|
|
768
|
+
jira_issue_key: The key of the Jira issue, e.g. "TEST-123
|
|
769
|
+
attachment_pattern: Optional regex pattern to filter attachments by filename.
|
|
770
|
+
If provided, only attachments with filenames matching this pattern will be processed.
|
|
771
|
+
Returns:
|
|
772
|
+
A string containing the content of all relevant attachments, separated by double newlines.
|
|
773
|
+
"""
|
|
730
774
|
|
|
731
775
|
attachment_data = []
|
|
732
776
|
attachments = self._client.get_attachments_ids_from_issue(issue=jira_issue_key)
|
|
777
|
+
api_version = str(getattr(self._client, "api_version", "2"))
|
|
733
778
|
for attachment in attachments:
|
|
734
|
-
if
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
779
|
+
if attachment_pattern and not re.search(attachment_pattern, attachment['filename']):
|
|
780
|
+
logger.info(f"Skipping attachment {attachment['filename']} as it does not match pattern {attachment_pattern}")
|
|
781
|
+
continue
|
|
782
|
+
logger.info(f"Processing attachment {attachment['filename']} with ID {attachment['attachment_id']}")
|
|
783
|
+
try:
|
|
784
|
+
attachment_content = None
|
|
785
|
+
|
|
786
|
+
# Cloud (REST v3) attachments require signed URLs returned from metadata
|
|
787
|
+
if api_version in {"3", "latest"} or self.cloud:
|
|
788
|
+
attachment_content = self._download_attachment_v3(
|
|
789
|
+
attachment['attachment_id'],
|
|
790
|
+
attachment['filename']
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
if attachment_content is None:
|
|
794
|
+
attachment_content = self._client.get_attachment_content(attachment['attachment_id'])
|
|
795
|
+
except Exception as e:
|
|
796
|
+
logger.error(
|
|
797
|
+
f"Failed to download attachment {attachment['filename']} for issue {jira_issue_key}: {str(e)}")
|
|
798
|
+
attachment_content = self._client.get(
|
|
799
|
+
path=f"secure/attachment/{attachment['attachment_id']}/{attachment['filename']}", not_json_response=True)
|
|
800
|
+
content_docs = process_content_by_type(attachment_content, attachment['filename'], llm=self.llm, fallback_extensions=[".txt", ".png"])
|
|
801
|
+
attachment_data.append("filename: " + attachment['filename'] + "\ncontent: " + str([doc.page_content for doc in content_docs]))
|
|
802
|
+
|
|
740
803
|
return "\n\n".join(attachment_data)
|
|
741
804
|
|
|
742
805
|
def execute_generic_rq(self, method: str, relative_url: str, params: Optional[str] = "", *args):
|
|
@@ -770,15 +833,6 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
770
833
|
logger.debug(response_string)
|
|
771
834
|
return response_string
|
|
772
835
|
|
|
773
|
-
def _extract_attachment_content(self, attachment):
|
|
774
|
-
"""Extract attachment's content if possible (used for api v.2)"""
|
|
775
|
-
|
|
776
|
-
try:
|
|
777
|
-
content = self._client.get(attachment['content'].replace(self.base_url, ''))
|
|
778
|
-
except Exception as e:
|
|
779
|
-
content = f"Unable to parse content of '{attachment['filename']}' due to: {str(e)}"
|
|
780
|
-
return f"filename: {attachment['filename']}\ncontent: {content}"
|
|
781
|
-
|
|
782
836
|
# Helper functions for image processing
|
|
783
837
|
@staticmethod
|
|
784
838
|
def _collect_context_for_image(content: str, image_marker: str, context_radius: int = 500) -> str:
|
|
@@ -1011,13 +1065,65 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1011
1065
|
logger.error(f"Error downloading attachment: {str(e)}")
|
|
1012
1066
|
return None
|
|
1013
1067
|
|
|
1068
|
+
def _download_attachment_v3(self, attachment_id: str, filename: str | None = None) -> Optional[bytes]:
|
|
1069
|
+
"""Download Jira attachment using metadata content URL (required for REST v3 / Cloud)."""
|
|
1070
|
+
try:
|
|
1071
|
+
metadata = self._client.get_attachment(attachment_id)
|
|
1072
|
+
except Exception as e:
|
|
1073
|
+
logger.error(f"Failed to retrieve metadata for attachment {attachment_id}: {str(e)}")
|
|
1074
|
+
return None
|
|
1075
|
+
|
|
1076
|
+
download_url = metadata.get('content') or metadata.get('_links', {}).get('content')
|
|
1077
|
+
|
|
1078
|
+
if not download_url:
|
|
1079
|
+
logger.warning(
|
|
1080
|
+
f"Attachment {attachment_id} ({filename}) metadata does not include a content URL; falling back.")
|
|
1081
|
+
return None
|
|
1082
|
+
|
|
1083
|
+
logger.info(f"Downloading attachment {attachment_id} via metadata content URL (v3).")
|
|
1084
|
+
content = self._download_attachment(download_url)
|
|
1085
|
+
|
|
1086
|
+
if content is None:
|
|
1087
|
+
logger.error(
|
|
1088
|
+
f"Failed to download attachment {attachment_id} ({filename}) from v3 content URL: {download_url}")
|
|
1089
|
+
|
|
1090
|
+
return content
|
|
1091
|
+
|
|
1014
1092
|
def _extract_image_data(self, field_data):
|
|
1015
1093
|
"""
|
|
1016
|
-
Extracts image data from general JSON response
|
|
1094
|
+
Extracts image data from general JSON response.
|
|
1095
|
+
Handles lists, dicts with image info, and plain strings.
|
|
1017
1096
|
"""
|
|
1018
|
-
if isinstance(field_data,
|
|
1019
|
-
return
|
|
1020
|
-
|
|
1097
|
+
if isinstance(field_data, list):
|
|
1098
|
+
return ' '.join(self._extract_image_data(item) for item in field_data)
|
|
1099
|
+
if isinstance(field_data, dict):
|
|
1100
|
+
if 'filename' in field_data and 'content' in field_data:
|
|
1101
|
+
return f"!{field_data['filename']}|alt={field_data['filename']}!"
|
|
1102
|
+
if 'content' in field_data and isinstance(field_data['content'], list):
|
|
1103
|
+
result = []
|
|
1104
|
+
for content_item in field_data['content']:
|
|
1105
|
+
if (
|
|
1106
|
+
isinstance(content_item, dict)
|
|
1107
|
+
and 'content' in content_item
|
|
1108
|
+
and isinstance(content_item['content'], list)
|
|
1109
|
+
and content_item['content']
|
|
1110
|
+
):
|
|
1111
|
+
if content_item.get('type') == 'mediaSingle':
|
|
1112
|
+
media = content_item['content'][0]
|
|
1113
|
+
attrs = media.get('attrs', {})
|
|
1114
|
+
if attrs.get('type') == 'file':
|
|
1115
|
+
alt = attrs.get('alt', '')
|
|
1116
|
+
image_str = f'!{alt}|alt="{alt}"!'
|
|
1117
|
+
result.append(image_str)
|
|
1118
|
+
elif content_item.get('type') == 'paragraph':
|
|
1119
|
+
result.append(content_item['content'][0].get('text', ''))
|
|
1120
|
+
else:
|
|
1121
|
+
result.append(self._extract_image_data(content_item))
|
|
1122
|
+
return '\n'.join(result)
|
|
1123
|
+
return f"Unsupported format of field content."
|
|
1124
|
+
if isinstance(field_data, str):
|
|
1125
|
+
return field_data
|
|
1126
|
+
return f"Unsupported field content type: {type(field_data)}. Expected a string, list, or dict."
|
|
1021
1127
|
|
|
1022
1128
|
def get_field_with_image_descriptions(self, jira_issue_key: str, field_name: str, prompt: Optional[str] = None,
|
|
1023
1129
|
context_radius: int = 500):
|
|
@@ -1052,12 +1158,7 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1052
1158
|
return f"Unable to find field '{field_name}' or it's empty. Available fields are: {existing_fields_str}"
|
|
1053
1159
|
|
|
1054
1160
|
# Handle multiple images or non-string content
|
|
1055
|
-
|
|
1056
|
-
field_content = ' '.join(map(self._extract_image_data, field_content))
|
|
1057
|
-
elif isinstance(field_content, dict):
|
|
1058
|
-
field_content = self._extract_image_data(field_content)
|
|
1059
|
-
elif not isinstance(field_content, str):
|
|
1060
|
-
return f"Unsupported field content type: {type(field_content)}. Expected a string, list, or dict."
|
|
1161
|
+
field_content = self._extract_image_data(field_content)
|
|
1061
1162
|
|
|
1062
1163
|
# Regular expression to find image references in Jira markup
|
|
1063
1164
|
image_pattern = r'!([^!|]+)(?:\|[^!]*)?!'
|
|
@@ -1118,6 +1219,97 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1118
1219
|
logger.error(f"Error processing field with images: {stacktrace}")
|
|
1119
1220
|
return f"Error processing field with images: {str(e)}"
|
|
1120
1221
|
|
|
1222
|
+
def process_image_match(self, match, body, attachment_resolver, context_radius=500, prompt=None):
|
|
1223
|
+
"""Process each image reference and get its contextual description"""
|
|
1224
|
+
image_ref = match.group(1)
|
|
1225
|
+
full_match = match.group(0) # The complete image reference with markers
|
|
1226
|
+
|
|
1227
|
+
logger.info(f"Processing image reference: {image_ref} (full match: {full_match})")
|
|
1228
|
+
|
|
1229
|
+
try:
|
|
1230
|
+
# Use the AttachmentResolver to find the attachment
|
|
1231
|
+
attachment = attachment_resolver.find_attachment(image_ref)
|
|
1232
|
+
|
|
1233
|
+
if not attachment:
|
|
1234
|
+
logger.warning(f"Could not find attachment for reference: {image_ref}")
|
|
1235
|
+
if image_ref.startswith("http://") or image_ref.startswith("https://"):
|
|
1236
|
+
content_url = image_ref
|
|
1237
|
+
image_name = image_ref.split("/")[-1] # Extract the name from the URL
|
|
1238
|
+
response = requests.get(content_url, timeout=10)
|
|
1239
|
+
response.raise_for_status()
|
|
1240
|
+
image_data = response.content
|
|
1241
|
+
else:
|
|
1242
|
+
logger.error(f"Invalid image reference: {image_ref}")
|
|
1243
|
+
return f"[Image: {image_ref} - attachment not found]"
|
|
1244
|
+
else:
|
|
1245
|
+
# Get the content URL and download the image
|
|
1246
|
+
content_url = attachment.get('content')
|
|
1247
|
+
if not content_url:
|
|
1248
|
+
logger.error(f"No content URL found in attachment: {attachment}")
|
|
1249
|
+
return f"[Image: {image_ref} - no content URL]"
|
|
1250
|
+
|
|
1251
|
+
image_name = attachment.get('filename', image_ref)
|
|
1252
|
+
|
|
1253
|
+
# Download the image data
|
|
1254
|
+
logger.info(f"Downloading image from URL: {content_url}")
|
|
1255
|
+
image_data = self._download_attachment(content_url)
|
|
1256
|
+
|
|
1257
|
+
if not image_data:
|
|
1258
|
+
logger.error(f"Failed to download image from URL: {content_url}")
|
|
1259
|
+
return f"[Image: {image_ref} - download failed]"
|
|
1260
|
+
|
|
1261
|
+
# Collect surrounding content
|
|
1262
|
+
context_text = self._collect_context_for_image(body, full_match, context_radius)
|
|
1263
|
+
|
|
1264
|
+
# Process with LLM (will use cache if available)
|
|
1265
|
+
description = self._process_image_with_llm(image_data, image_name, context_text, prompt)
|
|
1266
|
+
return f"[Image {image_name} Description: {description}]"
|
|
1267
|
+
|
|
1268
|
+
except Exception as e:
|
|
1269
|
+
logger.error(f"Error retrieving attachment {image_ref}: {str(e)}")
|
|
1270
|
+
return f"[Image: {image_ref} - Error: {str(e)}]"
|
|
1271
|
+
|
|
1272
|
+
def get_processed_comments_list_with_image_description(self, jira_issue_key: str, prompt: Optional[str] = None, context_radius: int = 500):
|
|
1273
|
+
# Retrieve all comments for the issue
|
|
1274
|
+
comments = self._client.issue_get_comments(jira_issue_key)
|
|
1275
|
+
|
|
1276
|
+
if not comments or not comments.get('comments'):
|
|
1277
|
+
return []
|
|
1278
|
+
|
|
1279
|
+
processed_comments = []
|
|
1280
|
+
|
|
1281
|
+
# Create an AttachmentResolver to efficiently handle attachment lookups
|
|
1282
|
+
attachment_resolver = AttachmentResolver(self._client, jira_issue_key)
|
|
1283
|
+
|
|
1284
|
+
# Regular expression to find image references in Jira markup
|
|
1285
|
+
image_pattern = r'!([^!|]+)(?:\|[^!]*)?!'
|
|
1286
|
+
|
|
1287
|
+
# Process each comment
|
|
1288
|
+
for comment in comments['comments']:
|
|
1289
|
+
comment_body = comment.get('body', '')
|
|
1290
|
+
if not comment_body:
|
|
1291
|
+
continue
|
|
1292
|
+
|
|
1293
|
+
comment_author = comment.get('author', {}).get('displayName', 'Unknown')
|
|
1294
|
+
comment_created = comment.get('created', 'Unknown date')
|
|
1295
|
+
comment_body = self._extract_image_data(comment_body)
|
|
1296
|
+
|
|
1297
|
+
# Process the comment body by replacing image references with descriptions
|
|
1298
|
+
processed_body = re.sub(image_pattern,
|
|
1299
|
+
lambda match: self.process_image_match(match, comment_body, attachment_resolver, context_radius, prompt),
|
|
1300
|
+
comment_body)
|
|
1301
|
+
|
|
1302
|
+
# Add the processed comment to our results
|
|
1303
|
+
processed_comments.append({
|
|
1304
|
+
"author": comment_author,
|
|
1305
|
+
"created": comment_created,
|
|
1306
|
+
"id": comment.get('id'),
|
|
1307
|
+
"original_content": comment_body,
|
|
1308
|
+
"processed_content": processed_body
|
|
1309
|
+
})
|
|
1310
|
+
|
|
1311
|
+
return processed_comments
|
|
1312
|
+
|
|
1121
1313
|
def get_comments_with_image_descriptions(self, jira_issue_key: str, prompt: Optional[str] = None, context_radius: int = 500):
|
|
1122
1314
|
"""
|
|
1123
1315
|
Get all comments from Jira issue and augment any images in them with textual descriptions.
|
|
@@ -1137,84 +1329,11 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1137
1329
|
The comments with image references replaced with contextual descriptions
|
|
1138
1330
|
"""
|
|
1139
1331
|
try:
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
if not
|
|
1332
|
+
processed_comments = self.get_processed_comments_list_with_image_description(jira_issue_key=jira_issue_key,
|
|
1333
|
+
prompt=prompt,
|
|
1334
|
+
context_radius=context_radius)
|
|
1335
|
+
if not processed_comments:
|
|
1144
1336
|
return f"No comments found for issue '{jira_issue_key}'"
|
|
1145
|
-
|
|
1146
|
-
processed_comments = []
|
|
1147
|
-
|
|
1148
|
-
# Create an AttachmentResolver to efficiently handle attachment lookups
|
|
1149
|
-
attachment_resolver = AttachmentResolver(self._client, jira_issue_key)
|
|
1150
|
-
|
|
1151
|
-
# Regular expression to find image references in Jira markup
|
|
1152
|
-
image_pattern = r'!([^!|]+)(?:\|[^!]*)?!'
|
|
1153
|
-
|
|
1154
|
-
# Process each comment
|
|
1155
|
-
for comment in comments['comments']:
|
|
1156
|
-
comment_body = comment.get('body', '')
|
|
1157
|
-
if not comment_body:
|
|
1158
|
-
continue
|
|
1159
|
-
|
|
1160
|
-
comment_author = comment.get('author', {}).get('displayName', 'Unknown')
|
|
1161
|
-
comment_created = comment.get('created', 'Unknown date')
|
|
1162
|
-
|
|
1163
|
-
# Function to process images in comment text
|
|
1164
|
-
def process_image_match(match):
|
|
1165
|
-
"""Process each image reference and get its contextual description"""
|
|
1166
|
-
image_ref = match.group(1)
|
|
1167
|
-
full_match = match.group(0) # The complete image reference with markers
|
|
1168
|
-
|
|
1169
|
-
logger.info(f"Processing image reference: {image_ref} (full match: {full_match})")
|
|
1170
|
-
|
|
1171
|
-
try:
|
|
1172
|
-
# Use the AttachmentResolver to find the attachment
|
|
1173
|
-
attachment = attachment_resolver.find_attachment(image_ref)
|
|
1174
|
-
|
|
1175
|
-
if not attachment:
|
|
1176
|
-
logger.warning(f"Could not find attachment for reference: {image_ref}")
|
|
1177
|
-
return f"[Image: {image_ref} - attachment not found]"
|
|
1178
|
-
|
|
1179
|
-
# Get the content URL and download the image
|
|
1180
|
-
content_url = attachment.get('content')
|
|
1181
|
-
if not content_url:
|
|
1182
|
-
logger.error(f"No content URL found in attachment: {attachment}")
|
|
1183
|
-
return f"[Image: {image_ref} - no content URL]"
|
|
1184
|
-
|
|
1185
|
-
image_name = attachment.get('filename', image_ref)
|
|
1186
|
-
|
|
1187
|
-
# Collect surrounding content
|
|
1188
|
-
context_text = self._collect_context_for_image(comment_body, full_match, context_radius)
|
|
1189
|
-
|
|
1190
|
-
# Download the image data
|
|
1191
|
-
logger.info(f"Downloading image from URL: {content_url}")
|
|
1192
|
-
image_data = self._download_attachment(content_url)
|
|
1193
|
-
|
|
1194
|
-
if not image_data:
|
|
1195
|
-
logger.error(f"Failed to download image from URL: {content_url}")
|
|
1196
|
-
return f"[Image: {image_ref} - download failed]"
|
|
1197
|
-
|
|
1198
|
-
# Process with LLM (will use cache if available)
|
|
1199
|
-
description = self._process_image_with_llm(image_data, image_name, context_text, prompt)
|
|
1200
|
-
return f"[Image {image_name} Description: {description}]"
|
|
1201
|
-
|
|
1202
|
-
except Exception as e:
|
|
1203
|
-
logger.error(f"Error retrieving attachment {image_ref}: {str(e)}")
|
|
1204
|
-
return f"[Image: {image_ref} - Error: {str(e)}]"
|
|
1205
|
-
|
|
1206
|
-
# Process the comment body by replacing image references with descriptions
|
|
1207
|
-
processed_body = re.sub(image_pattern, process_image_match, comment_body)
|
|
1208
|
-
|
|
1209
|
-
# Add the processed comment to our results
|
|
1210
|
-
processed_comments.append({
|
|
1211
|
-
"author": comment_author,
|
|
1212
|
-
"created": comment_created,
|
|
1213
|
-
"id": comment.get('id'),
|
|
1214
|
-
"original_content": comment_body,
|
|
1215
|
-
"processed_content": processed_body
|
|
1216
|
-
})
|
|
1217
|
-
|
|
1218
1337
|
# Format the output
|
|
1219
1338
|
result = f"Comments from issue '{jira_issue_key}' with image descriptions:\n\n"
|
|
1220
1339
|
for idx, comment in enumerate(processed_comments, 1):
|
|
@@ -1243,6 +1362,8 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1243
1362
|
self._skipped_attachment_extensions = kwargs.get('skip_attachment_extensions', [])
|
|
1244
1363
|
self._include_attachments = kwargs.get('include_attachments', False)
|
|
1245
1364
|
self._included_fields = fields_to_extract.copy() if fields_to_extract else []
|
|
1365
|
+
self._include_comments = kwargs.get('include_comments', True)
|
|
1366
|
+
self._chunking_tool = kwargs.get('chunking_tool', None)
|
|
1246
1367
|
|
|
1247
1368
|
try:
|
|
1248
1369
|
# Prepare fields to extract
|
|
@@ -1257,7 +1378,7 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1257
1378
|
|
|
1258
1379
|
# Use provided JQL query or default to all issues
|
|
1259
1380
|
if not jql:
|
|
1260
|
-
jql_query = "ORDER BY updated DESC" # Default to get all issues ordered by update time
|
|
1381
|
+
jql_query = "created >= \"1970-01-01\" ORDER BY updated DESC" # Default to get all issues ordered by update time
|
|
1261
1382
|
else:
|
|
1262
1383
|
jql_query = jql
|
|
1263
1384
|
|
|
@@ -1285,6 +1406,19 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1285
1406
|
logger.error(f"Error loading Jira issues: {str(e)}")
|
|
1286
1407
|
raise ToolException(f"Unable to load Jira issues: {str(e)}")
|
|
1287
1408
|
|
|
1409
|
+
def _extend_data(self, documents: Generator[Document, None, None]):
|
|
1410
|
+
image_pattern = r'!([^!|]+)(?:\|[^!]*)?!'
|
|
1411
|
+
for doc in documents:
|
|
1412
|
+
attachment_resolver = AttachmentResolver(self._client, doc.metadata['issue_key'])
|
|
1413
|
+
processed_content = re.sub(image_pattern,
|
|
1414
|
+
lambda match: self.process_image_match(match,
|
|
1415
|
+
doc.page_content,
|
|
1416
|
+
attachment_resolver),
|
|
1417
|
+
doc.page_content)
|
|
1418
|
+
doc.metadata[IndexerKeywords.CONTENT_IN_BYTES.value] = processed_content.encode('utf-8')
|
|
1419
|
+
doc.metadata[IndexerKeywords.CONTENT_FILE_NAME.value] = f"base_doc{file_extension_by_chunker(self._chunking_tool)}"
|
|
1420
|
+
yield doc
|
|
1421
|
+
|
|
1288
1422
|
def _process_document(self, base_document: Document) -> Generator[Document, None, None]:
|
|
1289
1423
|
"""
|
|
1290
1424
|
Process a base document to extract and index Jira issues extra fields: comments, attachments, etc..
|
|
@@ -1306,21 +1440,36 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1306
1440
|
except Exception as e:
|
|
1307
1441
|
logger.error(f"Failed to download attachment {attachment['filename']} for issue {issue_key}: {str(e)}")
|
|
1308
1442
|
attachment_content = self._client.get(path=f"secure/attachment/{attachment['id']}/{attachment['filename']}", not_json_response=True)
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1443
|
+
|
|
1444
|
+
yield Document(page_content='',
|
|
1445
|
+
metadata={
|
|
1446
|
+
IndexerKeywords.CONTENT_IN_BYTES.value: attachment_content,
|
|
1447
|
+
IndexerKeywords.CONTENT_FILE_NAME.value: attachment['filename'],
|
|
1448
|
+
'id': attachment_id,
|
|
1449
|
+
'issue_key': issue_key,
|
|
1450
|
+
'source': f"{self.base_url}/browse/{issue_key}",
|
|
1451
|
+
'filename': attachment['filename'],
|
|
1452
|
+
'created': attachment['created'],
|
|
1453
|
+
'mimeType': attachment['mimeType'],
|
|
1454
|
+
'author': attachment.get('author', {}).get('name'),
|
|
1455
|
+
IndexerKeywords.PARENT.value: base_document.metadata.get('id', None),
|
|
1456
|
+
'type': 'attachment',
|
|
1457
|
+
})
|
|
1458
|
+
if self._include_comments:
|
|
1459
|
+
comments = self.get_processed_comments_list_with_image_description(issue_key)
|
|
1460
|
+
if comments:
|
|
1461
|
+
for comment in comments:
|
|
1462
|
+
yield Document(page_content='',
|
|
1314
1463
|
metadata={
|
|
1315
|
-
'
|
|
1464
|
+
IndexerKeywords.CONTENT_IN_BYTES.value: comment.get('processed_content').encode('utf-8'),
|
|
1465
|
+
IndexerKeywords.CONTENT_FILE_NAME.value: "comment.md",
|
|
1466
|
+
'id': comment.get('id'),
|
|
1316
1467
|
'issue_key': issue_key,
|
|
1317
1468
|
'source': f"{self.base_url}/browse/{issue_key}",
|
|
1318
|
-
'
|
|
1319
|
-
'
|
|
1320
|
-
'mimeType': attachment['mimeType'],
|
|
1321
|
-
'author': attachment.get('author', {}).get('name'),
|
|
1469
|
+
'created': comment.get('created'),
|
|
1470
|
+
'author': comment.get('author'),
|
|
1322
1471
|
IndexerKeywords.PARENT.value: base_document.metadata.get('id', None),
|
|
1323
|
-
'type': '
|
|
1472
|
+
'type': 'comment',
|
|
1324
1473
|
})
|
|
1325
1474
|
|
|
1326
1475
|
def _jql_get_tickets(self, jql, fields="*all", start=0, limit=None, expand=None, validate_query=None):
|
|
@@ -1343,7 +1492,7 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1343
1492
|
if validate_query is not None:
|
|
1344
1493
|
params["validateQuery"] = validate_query
|
|
1345
1494
|
|
|
1346
|
-
url = self._client.resource_url("search")
|
|
1495
|
+
url = self._client.resource_url("search/jql" if self.api_version == '3' else "search")
|
|
1347
1496
|
|
|
1348
1497
|
while True:
|
|
1349
1498
|
params["startAt"] = int(start)
|
|
@@ -1361,6 +1510,8 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1361
1510
|
break
|
|
1362
1511
|
if not response["issues"]:
|
|
1363
1512
|
break
|
|
1513
|
+
if len(issues) < limit:
|
|
1514
|
+
break
|
|
1364
1515
|
start += len(issues)
|
|
1365
1516
|
|
|
1366
1517
|
def _process_issue_for_indexing(self, issue: dict, fields_to_index=None) -> Document:
|
|
@@ -1370,21 +1521,16 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1370
1521
|
"""
|
|
1371
1522
|
try:
|
|
1372
1523
|
# Build content starting with summary
|
|
1373
|
-
content = f"{issue['fields']['summary']}\n"
|
|
1524
|
+
content = f"# Summary\n{issue['fields']['summary']}\n\n"
|
|
1374
1525
|
|
|
1375
1526
|
# Add description if present
|
|
1376
1527
|
description = issue['fields'].get('description', '')
|
|
1377
1528
|
if description:
|
|
1378
|
-
content += f"{description}\n"
|
|
1529
|
+
content += f"# Description\n{description}\n\n"
|
|
1379
1530
|
else:
|
|
1380
1531
|
# If no description, still create document but with minimal content
|
|
1381
1532
|
logger.debug(f"Issue {issue.get('key', 'unknown')} has no description")
|
|
1382
1533
|
|
|
1383
|
-
# Add comments if present
|
|
1384
|
-
if 'comment' in issue['fields'] and issue['fields']['comment'].get('comments'):
|
|
1385
|
-
for comment in issue['fields']['comment']['comments']:
|
|
1386
|
-
content += f"{comment['body']}\n"
|
|
1387
|
-
|
|
1388
1534
|
# Add additional fields to index
|
|
1389
1535
|
if fields_to_index:
|
|
1390
1536
|
for field in fields_to_index:
|
|
@@ -1395,7 +1541,7 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1395
1541
|
field_value = str(field_value)
|
|
1396
1542
|
elif isinstance(field_value, list):
|
|
1397
1543
|
field_value = ', '.join(str(item) for item in field_value)
|
|
1398
|
-
content += f"{field_value}\n"
|
|
1544
|
+
content += f"# {field}\n{field_value}\n\n"
|
|
1399
1545
|
|
|
1400
1546
|
# Create metadata
|
|
1401
1547
|
metadata = {
|
|
@@ -1433,6 +1579,7 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1433
1579
|
'skip_attachment_extensions': (Optional[List[str]], Field(
|
|
1434
1580
|
description="List of file extensions to skip when processing attachments: i.e. ['.png', '.jpg']",
|
|
1435
1581
|
default=[])),
|
|
1582
|
+
'chunking_tool': (Literal['markdown', ''], Field(description="Name of chunking tool for base document", default='markdown')),
|
|
1436
1583
|
}
|
|
1437
1584
|
|
|
1438
1585
|
# def index_data(self,
|
|
@@ -1505,7 +1652,7 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1505
1652
|
# logger.error(f"Error indexing Jira issues: {str(e)}")
|
|
1506
1653
|
# raise ToolException(f"Error indexing Jira issues: {str(e)}")
|
|
1507
1654
|
|
|
1508
|
-
@
|
|
1655
|
+
@extend_with_parent_available_tools
|
|
1509
1656
|
def get_available_tools(self):
|
|
1510
1657
|
return [
|
|
1511
1658
|
{
|
|
@@ -1589,7 +1736,7 @@ class JiraApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
1589
1736
|
{
|
|
1590
1737
|
"name": "get_attachments_content",
|
|
1591
1738
|
"description": self.get_attachments_content.__doc__,
|
|
1592
|
-
"args_schema":
|
|
1739
|
+
"args_schema": GetIssueAttachments,
|
|
1593
1740
|
"ref": self.get_attachments_content,
|
|
1594
1741
|
},
|
|
1595
1742
|
{
|