naas-abi 1.0.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.
- naas_abi/__init__.py +35 -0
- naas_abi/agents/AbiAgent.py +442 -0
- naas_abi/agents/AbiAgent_test.py +157 -0
- naas_abi/agents/EntitytoSPARQLAgent.py +952 -0
- naas_abi/agents/EntitytoSPARQLAgent_test.py +66 -0
- naas_abi/agents/KnowledgeGraphBuilderAgent.py +321 -0
- naas_abi/agents/KnowledgeGraphBuilderAgent_test.py +86 -0
- naas_abi/agents/OntologyEngineerAgent.py +115 -0
- naas_abi/agents/OntologyEngineerAgent_test.py +42 -0
- naas_abi/apps/oxigraph_admin/main.py +392 -0
- naas_abi/apps/oxigraph_admin/terminal_style.py +151 -0
- naas_abi/apps/sparql_terminal/main.py +68 -0
- naas_abi/apps/sparql_terminal/terminal_style.py +236 -0
- naas_abi/apps/terminal_agent/main.py +553 -0
- naas_abi/apps/terminal_agent/terminal_style.py +175 -0
- naas_abi/cli.py +714 -0
- naas_abi/mappings.py +83 -0
- naas_abi/models/airgap_gemma.py +220 -0
- naas_abi/models/airgap_qwen.py +24 -0
- naas_abi/models/default.py +23 -0
- naas_abi/models/gpt_4_1.py +25 -0
- naas_abi/pipelines/AIAgentOntologyGenerationPipeline.py +635 -0
- naas_abi/pipelines/AIAgentOntologyGenerationPipeline_test.py +133 -0
- naas_abi/pipelines/AddIndividualPipeline.py +215 -0
- naas_abi/pipelines/AddIndividualPipeline_test.py +66 -0
- naas_abi/pipelines/InsertDataSPARQLPipeline.py +197 -0
- naas_abi/pipelines/InsertDataSPARQLPipeline_test.py +96 -0
- naas_abi/pipelines/MergeIndividualsPipeline.py +245 -0
- naas_abi/pipelines/MergeIndividualsPipeline_test.py +98 -0
- naas_abi/pipelines/RemoveIndividualPipeline.py +166 -0
- naas_abi/pipelines/RemoveIndividualPipeline_test.py +58 -0
- naas_abi/pipelines/UpdateCommercialOrganizationPipeline.py +198 -0
- naas_abi/pipelines/UpdateDataPropertyPipeline.py +175 -0
- naas_abi/pipelines/UpdateLegalNamePipeline.py +107 -0
- naas_abi/pipelines/UpdateLinkedInPagePipeline.py +179 -0
- naas_abi/pipelines/UpdatePersonPipeline.py +184 -0
- naas_abi/pipelines/UpdateSkillPipeline.py +118 -0
- naas_abi/pipelines/UpdateTickerPipeline.py +104 -0
- naas_abi/pipelines/UpdateWebsitePipeline.py +106 -0
- naas_abi/triggers.py +131 -0
- naas_abi/workflows/AgentRecommendationWorkflow.py +321 -0
- naas_abi/workflows/AgentRecommendationWorkflow_test.py +160 -0
- naas_abi/workflows/ArtificialAnalysisWorkflow.py +337 -0
- naas_abi/workflows/ArtificialAnalysisWorkflow_test.py +57 -0
- naas_abi/workflows/ConvertOntologyGraphToYamlWorkflow.py +210 -0
- naas_abi/workflows/ConvertOntologyGraphToYamlWorkflow_test.py +78 -0
- naas_abi/workflows/CreateClassOntologyYamlWorkflow.py +208 -0
- naas_abi/workflows/CreateClassOntologyYamlWorkflow_test.py +65 -0
- naas_abi/workflows/CreateIndividualOntologyYamlWorkflow.py +183 -0
- naas_abi/workflows/CreateIndividualOntologyYamlWorkflow_test.py +86 -0
- naas_abi/workflows/ExportGraphInstancesToExcelWorkflow.py +450 -0
- naas_abi/workflows/ExportGraphInstancesToExcelWorkflow_test.py +33 -0
- naas_abi/workflows/GetObjectPropertiesFromClassWorkflow.py +385 -0
- naas_abi/workflows/GetObjectPropertiesFromClassWorkflow_test.py +57 -0
- naas_abi/workflows/GetSubjectGraphWorkflow.py +84 -0
- naas_abi/workflows/GetSubjectGraphWorkflow_test.py +71 -0
- naas_abi/workflows/SearchIndividualWorkflow.py +190 -0
- naas_abi/workflows/SearchIndividualWorkflow_test.py +98 -0
- naas_abi-1.0.0.dist-info/METADATA +9 -0
- naas_abi-1.0.0.dist-info/RECORD +62 -0
- naas_abi-1.0.0.dist-info/WHEEL +5 -0
- naas_abi-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from fastapi import APIRouter
|
|
9
|
+
from langchain_core.tools import BaseTool, StructuredTool
|
|
10
|
+
from naas_abi_core.services.triple_store.TripleStorePorts import ITripleStoreService
|
|
11
|
+
from naas_abi_core.workflow import Workflow, WorkflowConfiguration, WorkflowParameters
|
|
12
|
+
from rdflib import Graph, Namespace
|
|
13
|
+
|
|
14
|
+
ABI = Namespace("http://naas.ai/ontology/abi#")
|
|
15
|
+
INTENT_MAPPING = Namespace("http://ontology.naas.ai/intentMapping/")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AgentRecommendationConfiguration(WorkflowConfiguration):
|
|
20
|
+
"""Configuration for Agent Recommendation Workflow.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
triple_store (ITripleStoreService): The ontology store service to use
|
|
24
|
+
oxigraph_url (str): URL of the Oxigraph SPARQL endpoint
|
|
25
|
+
queries_file_path (str): Path to the SPARQL queries TTL file
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
triple_store: ITripleStoreService
|
|
29
|
+
oxigraph_url: str = "http://localhost:7878"
|
|
30
|
+
queries_file_path: str = "src/core/modules/abi/ontologies/application-level/AgentRecommendationSparqlQueries.ttl"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AgentRecommendationParameters(WorkflowParameters):
|
|
34
|
+
"""Parameters for Agent Recommendation Workflow execution.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
intent_description (str): Natural language description of what the user wants
|
|
38
|
+
min_intelligence_score (int): Minimum intelligence index (0-100)
|
|
39
|
+
max_input_cost (float): Maximum input cost per million tokens
|
|
40
|
+
max_results (int): Maximum number of recommendations to return
|
|
41
|
+
provider_preference (str): Optional preferred provider name
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
intent_description: str
|
|
45
|
+
min_intelligence_score: Optional[int] = 10
|
|
46
|
+
max_input_cost: Optional[float] = None
|
|
47
|
+
max_results: Optional[int] = 10
|
|
48
|
+
provider_preference: Optional[str] = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AgentRecommendationWorkflow(Workflow):
|
|
52
|
+
__configuration: AgentRecommendationConfiguration
|
|
53
|
+
|
|
54
|
+
def __init__(self, configuration: AgentRecommendationConfiguration):
|
|
55
|
+
self.__configuration = configuration
|
|
56
|
+
self._load_queries()
|
|
57
|
+
|
|
58
|
+
def _load_queries(self) -> None:
|
|
59
|
+
"""Load SPARQL queries from the TTL file."""
|
|
60
|
+
self._queries = {}
|
|
61
|
+
|
|
62
|
+
# Load the TTL file and parse query templates
|
|
63
|
+
queries_file = Path(self.__configuration.queries_file_path)
|
|
64
|
+
if not queries_file.exists():
|
|
65
|
+
raise FileNotFoundError(f"Queries file not found: {queries_file}")
|
|
66
|
+
|
|
67
|
+
# Parse the RDF graph to extract query templates
|
|
68
|
+
graph = Graph()
|
|
69
|
+
graph.parse(queries_file, format="turtle")
|
|
70
|
+
|
|
71
|
+
# Extract templatable SPARQL queries
|
|
72
|
+
for subject in graph.subjects(predicate=INTENT_MAPPING.intentDescription):
|
|
73
|
+
query_id = str(subject).split("/")[-1].replace("Query", "")
|
|
74
|
+
intent_desc = str(graph.value(subject, INTENT_MAPPING.intentDescription))
|
|
75
|
+
sparql_template = str(graph.value(subject, INTENT_MAPPING.sparqlTemplate))
|
|
76
|
+
|
|
77
|
+
self._queries[query_id] = {
|
|
78
|
+
"intent_description": intent_desc,
|
|
79
|
+
"sparql_template": sparql_template,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def run_workflow(self, parameters: WorkflowParameters) -> Dict[str, Any]:
|
|
83
|
+
if not isinstance(parameters, AgentRecommendationParameters):
|
|
84
|
+
raise ValueError("Parameters must be of type AgentRecommendationParameters")
|
|
85
|
+
|
|
86
|
+
print(
|
|
87
|
+
f"🔍 [AgentRecommendation] Processing intent: '{parameters.intent_description}'"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Step 1: Match intent to appropriate query
|
|
91
|
+
selected_query = self._match_intent_to_query(parameters.intent_description)
|
|
92
|
+
print(
|
|
93
|
+
f"📋 [AgentRecommendation] Matched query: {selected_query['intent_description']}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Step 2: Template the SPARQL query with parameters
|
|
97
|
+
templated_query = self._template_query(selected_query, parameters)
|
|
98
|
+
print("🔧 [AgentRecommendation] Templated SPARQL query:")
|
|
99
|
+
print(templated_query)
|
|
100
|
+
|
|
101
|
+
# Step 3: Execute the query against Oxigraph
|
|
102
|
+
print("⚡ [AgentRecommendation] Executing SPARQL query against Oxigraph...")
|
|
103
|
+
results = self._execute_sparql_query(templated_query)
|
|
104
|
+
print(f"📊 [AgentRecommendation] Query returned {len(results)} raw results")
|
|
105
|
+
|
|
106
|
+
# Step 4: Format recommendations for user
|
|
107
|
+
recommendations = self._format_recommendations(results, parameters)
|
|
108
|
+
print(
|
|
109
|
+
f"✅ [AgentRecommendation] Formatted {len(recommendations)} recommendations"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
"intent": parameters.intent_description,
|
|
114
|
+
"query_used": selected_query["intent_description"],
|
|
115
|
+
"recommendations": recommendations,
|
|
116
|
+
"total_found": len(recommendations),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
def _match_intent_to_query(self, intent_description: str) -> Dict[str, str]:
|
|
120
|
+
"""Match user intent to the most appropriate SPARQL query."""
|
|
121
|
+
intent_lower = intent_description.lower()
|
|
122
|
+
|
|
123
|
+
# Intent matching logic
|
|
124
|
+
if any(
|
|
125
|
+
word in intent_lower
|
|
126
|
+
for word in ["business", "proposal", "professional", "document"]
|
|
127
|
+
):
|
|
128
|
+
return self._queries["abi#findBusinessProposalAgents"]
|
|
129
|
+
elif any(
|
|
130
|
+
word in intent_lower
|
|
131
|
+
for word in ["code", "programming", "development", "script"]
|
|
132
|
+
):
|
|
133
|
+
return self._queries["abi#findCodingAgents"]
|
|
134
|
+
elif any(
|
|
135
|
+
word in intent_lower
|
|
136
|
+
for word in ["math", "calculation", "equation", "statistics"]
|
|
137
|
+
):
|
|
138
|
+
return self._queries["abi#findMathAgents"]
|
|
139
|
+
elif any(word in intent_lower for word in ["fast", "quick", "speed", "rapid"]):
|
|
140
|
+
return self._queries["abi#findFastestAgents"]
|
|
141
|
+
elif any(word in intent_lower for word in ["cheap", "cost", "value", "budget"]):
|
|
142
|
+
return self._queries["abi#findBestValueAgents"]
|
|
143
|
+
else:
|
|
144
|
+
# Default to business proposal for general requests
|
|
145
|
+
return self._queries["abi#findBusinessProposalAgents"]
|
|
146
|
+
|
|
147
|
+
def _template_query(
|
|
148
|
+
self, query_info: Dict[str, str], parameters: AgentRecommendationParameters
|
|
149
|
+
) -> str:
|
|
150
|
+
"""Template the SPARQL query with user parameters."""
|
|
151
|
+
template = query_info["sparql_template"]
|
|
152
|
+
|
|
153
|
+
# Replace template variables
|
|
154
|
+
replacements: Dict[str, Any] = {
|
|
155
|
+
"min_intelligence_score": parameters.min_intelligence_score or 10,
|
|
156
|
+
"max_results": parameters.max_results or 10,
|
|
157
|
+
"min_coding_score": parameters.min_intelligence_score
|
|
158
|
+
or 10, # Use intelligence as fallback
|
|
159
|
+
"min_math_score": parameters.min_intelligence_score
|
|
160
|
+
or 10, # Use intelligence as fallback
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Add optional parameters
|
|
164
|
+
if parameters.max_input_cost:
|
|
165
|
+
replacements["max_input_cost"] = parameters.max_input_cost
|
|
166
|
+
|
|
167
|
+
if parameters.provider_preference:
|
|
168
|
+
replacements["provider_name"] = parameters.provider_preference
|
|
169
|
+
|
|
170
|
+
# Simple template replacement (for now - could use Jinja2 for more complex templating)
|
|
171
|
+
for key, value in replacements.items():
|
|
172
|
+
template = template.replace("{{ " + key + " }}", str(value))
|
|
173
|
+
|
|
174
|
+
# Handle conditional blocks (basic implementation)
|
|
175
|
+
template = self._handle_conditional_blocks(template, replacements)
|
|
176
|
+
|
|
177
|
+
return template
|
|
178
|
+
|
|
179
|
+
def _handle_conditional_blocks(
|
|
180
|
+
self, template: str, replacements: Dict[str, Any]
|
|
181
|
+
) -> str:
|
|
182
|
+
"""Handle {% if %} conditional blocks in templates."""
|
|
183
|
+
# Simple regex to find and process {% if variable %} blocks
|
|
184
|
+
|
|
185
|
+
# Pattern to match {% if variable %} ... {% endif %}
|
|
186
|
+
pattern = r"\{\%\s*if\s+(\w+)\s*\%\}(.*?)\{\%\s*endif\s*\%\}"
|
|
187
|
+
|
|
188
|
+
def replace_conditional(match):
|
|
189
|
+
variable = match.group(1)
|
|
190
|
+
content = match.group(2)
|
|
191
|
+
|
|
192
|
+
# Check if variable exists and has a truthy value in replacements
|
|
193
|
+
if variable in replacements and replacements[variable]:
|
|
194
|
+
return content
|
|
195
|
+
else:
|
|
196
|
+
return ""
|
|
197
|
+
|
|
198
|
+
return re.sub(pattern, replace_conditional, template, flags=re.DOTALL)
|
|
199
|
+
|
|
200
|
+
def _execute_sparql_query(self, sparql_query: str) -> List[Dict[str, Any]]:
|
|
201
|
+
"""Execute SPARQL query against Oxigraph."""
|
|
202
|
+
try:
|
|
203
|
+
response = requests.post(
|
|
204
|
+
f"{self.__configuration.oxigraph_url}/query",
|
|
205
|
+
headers={"Content-Type": "application/sparql-query"},
|
|
206
|
+
data=sparql_query,
|
|
207
|
+
)
|
|
208
|
+
response.raise_for_status()
|
|
209
|
+
|
|
210
|
+
data = response.json()
|
|
211
|
+
return data.get("results", {}).get("bindings", [])
|
|
212
|
+
|
|
213
|
+
except requests.RequestException as e:
|
|
214
|
+
raise RuntimeError(f"Failed to execute SPARQL query: {e}")
|
|
215
|
+
|
|
216
|
+
def _format_recommendations(
|
|
217
|
+
self, results: List[Dict[str, Any]], parameters: AgentRecommendationParameters
|
|
218
|
+
) -> List[Dict[str, Any]]:
|
|
219
|
+
"""Format query results into user-friendly recommendations."""
|
|
220
|
+
recommendations = []
|
|
221
|
+
|
|
222
|
+
for result in results:
|
|
223
|
+
recommendation = {
|
|
224
|
+
"agent": self._extract_value(result, "agentLabel")
|
|
225
|
+
or self._extract_value(result, "agent"),
|
|
226
|
+
"model": self._extract_value(result, "model"),
|
|
227
|
+
"provider": self._extract_value(result, "provider"),
|
|
228
|
+
"costs": {
|
|
229
|
+
"input_per_million_tokens": float(
|
|
230
|
+
self._extract_value(result, "inputCost") or 0
|
|
231
|
+
),
|
|
232
|
+
"output_per_million_tokens": float(
|
|
233
|
+
self._extract_value(result, "outputCost") or 0
|
|
234
|
+
),
|
|
235
|
+
"currency": "USD",
|
|
236
|
+
},
|
|
237
|
+
"performance": {
|
|
238
|
+
"intelligence_index": float(
|
|
239
|
+
self._extract_value(result, "intelligenceIndex") or 0
|
|
240
|
+
),
|
|
241
|
+
"coding_index": float(
|
|
242
|
+
self._extract_value(result, "codingIndex") or 0
|
|
243
|
+
),
|
|
244
|
+
"math_index": float(self._extract_value(result, "mathIndex") or 0),
|
|
245
|
+
"output_speed": float(
|
|
246
|
+
self._extract_value(result, "outputSpeed") or 0
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
"recommendation_reason": self._generate_recommendation_reason(
|
|
250
|
+
result, parameters.intent_description
|
|
251
|
+
),
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
recommendations.append(recommendation)
|
|
255
|
+
|
|
256
|
+
return recommendations
|
|
257
|
+
|
|
258
|
+
def _extract_value(self, result: Dict[str, Any], key: str) -> Optional[str]:
|
|
259
|
+
"""Extract value from SPARQL result binding."""
|
|
260
|
+
if key in result and "value" in result[key]:
|
|
261
|
+
return result[key]["value"]
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
def _generate_recommendation_reason(
|
|
265
|
+
self, result: Dict[str, Any], intent: str
|
|
266
|
+
) -> str:
|
|
267
|
+
"""Generate a human-readable reason for the recommendation."""
|
|
268
|
+
intelligence = float(self._extract_value(result, "intelligenceIndex") or 0)
|
|
269
|
+
coding = float(self._extract_value(result, "codingIndex") or 0)
|
|
270
|
+
math = float(self._extract_value(result, "mathIndex") or 0)
|
|
271
|
+
input_cost = float(self._extract_value(result, "inputCost") or 0)
|
|
272
|
+
|
|
273
|
+
reasons = []
|
|
274
|
+
|
|
275
|
+
if intelligence >= 80:
|
|
276
|
+
reasons.append("high intelligence score")
|
|
277
|
+
if coding >= 80:
|
|
278
|
+
reasons.append("excellent coding capabilities")
|
|
279
|
+
if math >= 80:
|
|
280
|
+
reasons.append("strong mathematical reasoning")
|
|
281
|
+
if input_cost <= 1.0:
|
|
282
|
+
reasons.append("cost-effective pricing")
|
|
283
|
+
|
|
284
|
+
if not reasons:
|
|
285
|
+
reasons.append("good overall performance")
|
|
286
|
+
|
|
287
|
+
return f"Recommended for {intent.lower()} due to {', '.join(reasons)}"
|
|
288
|
+
|
|
289
|
+
def as_tools(self) -> list[BaseTool]:
|
|
290
|
+
"""Returns a list of LangChain tools for this workflow.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
list[BaseTool]: List containing the workflow tool
|
|
294
|
+
"""
|
|
295
|
+
return [
|
|
296
|
+
StructuredTool(
|
|
297
|
+
name="recommend_ai_agents",
|
|
298
|
+
description="Recommend AI agents based on user intent and requirements. Analyzes capabilities, performance, and costs to suggest the best agents for specific tasks like business proposals, coding, math, etc.",
|
|
299
|
+
func=lambda **kwargs: self.run_workflow(
|
|
300
|
+
AgentRecommendationParameters(**kwargs)
|
|
301
|
+
),
|
|
302
|
+
args_schema=AgentRecommendationParameters,
|
|
303
|
+
)
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
def as_api(
|
|
307
|
+
self,
|
|
308
|
+
router: APIRouter,
|
|
309
|
+
route_name: str = "",
|
|
310
|
+
name: str = "",
|
|
311
|
+
description: str = "",
|
|
312
|
+
description_stream: str = "",
|
|
313
|
+
tags: list[str | Enum] | None = None,
|
|
314
|
+
) -> None:
|
|
315
|
+
if tags is None:
|
|
316
|
+
tags = []
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
def get_configuration(self) -> AgentRecommendationConfiguration:
|
|
320
|
+
"""Get the workflow configuration."""
|
|
321
|
+
return self.__configuration
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from unittest.mock import Mock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from naas_abi.workflows.AgentRecommendationWorkflow import (
|
|
5
|
+
AgentRecommendationConfiguration,
|
|
6
|
+
AgentRecommendationParameters,
|
|
7
|
+
AgentRecommendationWorkflow,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def workflow() -> AgentRecommendationWorkflow:
|
|
13
|
+
"""Create a test workflow instance."""
|
|
14
|
+
mock_triple_store = Mock()
|
|
15
|
+
|
|
16
|
+
workflow_configuration = AgentRecommendationConfiguration(
|
|
17
|
+
triple_store=mock_triple_store,
|
|
18
|
+
oxigraph_url="http://localhost:7878",
|
|
19
|
+
queries_file_path="src/core/modules/abi/ontologies/application-level/AgentRecommendationSparqlQueries.ttl",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
return AgentRecommendationWorkflow(workflow_configuration)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def sample_parameters() -> AgentRecommendationParameters:
|
|
27
|
+
"""Create sample workflow parameters."""
|
|
28
|
+
return AgentRecommendationParameters(
|
|
29
|
+
intent_description="I want to create a business proposal",
|
|
30
|
+
min_intelligence_score=75,
|
|
31
|
+
max_input_cost=5.0,
|
|
32
|
+
max_results=5,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_workflow_initialization(workflow):
|
|
37
|
+
"""Test that workflow initializes correctly and loads queries."""
|
|
38
|
+
assert workflow is not None
|
|
39
|
+
assert hasattr(workflow, "_queries")
|
|
40
|
+
# Note: This will fail if the TTL file doesn't exist, which is expected
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_intent_matching(workflow):
|
|
44
|
+
"""Test intent matching to appropriate queries."""
|
|
45
|
+
# Test business proposal matching
|
|
46
|
+
business_query = workflow._match_intent_to_query(
|
|
47
|
+
"I need to create a business proposal"
|
|
48
|
+
)
|
|
49
|
+
assert "business" in business_query["intent_description"].lower()
|
|
50
|
+
|
|
51
|
+
# Test coding matching
|
|
52
|
+
coding_query = workflow._match_intent_to_query("I want to write some Python code")
|
|
53
|
+
assert "coding" in coding_query["intent_description"].lower()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_template_replacement(workflow):
|
|
57
|
+
"""Test SPARQL template variable replacement."""
|
|
58
|
+
template = "SELECT * WHERE { ?x :score ?score . FILTER(?score >= {{ min_intelligence_score }}) } LIMIT {{ max_results }}"
|
|
59
|
+
parameters = AgentRecommendationParameters(
|
|
60
|
+
intent_description="test", min_intelligence_score=80, max_results=10
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
templated = workflow._template_query({"sparql_template": template}, parameters)
|
|
64
|
+
assert "80" in templated
|
|
65
|
+
assert "10" in templated
|
|
66
|
+
assert "{{" not in templated
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_conditional_blocks(workflow):
|
|
70
|
+
"""Test handling of conditional template blocks."""
|
|
71
|
+
template = """
|
|
72
|
+
SELECT * WHERE {
|
|
73
|
+
?x :hasProperty ?y .
|
|
74
|
+
{% if max_input_cost %}
|
|
75
|
+
FILTER(?cost <= {{ max_input_cost }})
|
|
76
|
+
{% endif %}
|
|
77
|
+
}
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
# Test with conditional parameter
|
|
81
|
+
replacements = {"max_input_cost": 5.0}
|
|
82
|
+
result = workflow._handle_conditional_blocks(template, replacements)
|
|
83
|
+
assert "FILTER(?cost" in result
|
|
84
|
+
|
|
85
|
+
# Test without conditional parameter
|
|
86
|
+
replacements = {}
|
|
87
|
+
result = workflow._handle_conditional_blocks(template, replacements)
|
|
88
|
+
assert "FILTER(?cost" not in result
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@patch("requests.post")
|
|
92
|
+
def test_sparql_execution(mock_post, workflow):
|
|
93
|
+
"""Test SPARQL query execution."""
|
|
94
|
+
# Mock successful response
|
|
95
|
+
mock_response = Mock()
|
|
96
|
+
mock_response.raise_for_status.return_value = None
|
|
97
|
+
mock_response.json.return_value = {
|
|
98
|
+
"results": {
|
|
99
|
+
"bindings": [
|
|
100
|
+
{
|
|
101
|
+
"agent": {"value": "Claude AI Agent"},
|
|
102
|
+
"provider": {"value": "Anthropic"},
|
|
103
|
+
"inputCost": {"value": "8.0"},
|
|
104
|
+
"intelligenceIndex": {"value": "85"},
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
mock_post.return_value = mock_response
|
|
110
|
+
|
|
111
|
+
results = workflow._execute_sparql_query("SELECT * WHERE { ?s ?p ?o }")
|
|
112
|
+
|
|
113
|
+
assert len(results) == 1
|
|
114
|
+
assert results[0]["agent"]["value"] == "Claude AI Agent"
|
|
115
|
+
mock_post.assert_called_once()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_recommendation_formatting(workflow):
|
|
119
|
+
"""Test formatting of SPARQL results into recommendations."""
|
|
120
|
+
raw_results = [
|
|
121
|
+
{
|
|
122
|
+
"agentLabel": {"value": "Claude AI Agent"},
|
|
123
|
+
"provider": {"value": "Anthropic"},
|
|
124
|
+
"inputCost": {"value": "8.0"},
|
|
125
|
+
"outputCost": {"value": "24.0"},
|
|
126
|
+
"intelligenceIndex": {"value": "85"},
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
parameters = AgentRecommendationParameters(
|
|
131
|
+
intent_description="create a business proposal"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
recommendations = workflow._format_recommendations(raw_results, parameters)
|
|
135
|
+
|
|
136
|
+
assert len(recommendations) == 1
|
|
137
|
+
recommendation = recommendations[0]
|
|
138
|
+
|
|
139
|
+
assert recommendation["agent"] == "Claude AI Agent"
|
|
140
|
+
assert recommendation["provider"] == "Anthropic"
|
|
141
|
+
assert recommendation["costs"]["input_per_million_tokens"] == 8.0
|
|
142
|
+
assert recommendation["performance"]["intelligence_index"] == 85.0
|
|
143
|
+
assert "business proposal" in recommendation["recommendation_reason"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_as_tools_returns_valid_tools(workflow):
|
|
147
|
+
"""Test that workflow returns valid LangChain tools."""
|
|
148
|
+
tools = workflow.as_tools()
|
|
149
|
+
|
|
150
|
+
assert len(tools) == 1
|
|
151
|
+
tool = tools[0]
|
|
152
|
+
|
|
153
|
+
assert tool.name == "recommend_ai_agents"
|
|
154
|
+
assert "AI agents" in tool.description
|
|
155
|
+
assert hasattr(tool, "func")
|
|
156
|
+
assert hasattr(tool, "args_schema")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
pytest.main([__file__])
|