ea4all-gradio-agents-mcp-hackathon-tools-refactoring-apm
Browse files- ea4all/ea4all_mcp.py +6 -5
- ea4all/src/ea4all_apm/graph.py +43 -75
- ea4all/src/ea4all_apm/state.py +22 -44
- ea4all/src/graph.py +4 -4
- ea4all/src/tools/tools.py +13 -7
- ea4all/utils/utils.py +8 -1
- pyproject.toml +3 -1
- requirements.txt +1 -0
- uv.lock +42 -0
ea4all/ea4all_mcp.py
CHANGED
@@ -25,7 +25,8 @@ from PIL import Image
|
|
25 |
|
26 |
from ea4all.utils.utils import (
|
27 |
UIUtils,
|
28 |
-
ea4all_agent_init, get_image, filter_page,
|
|
|
29 |
)
|
30 |
|
31 |
TITLE = """
|
@@ -40,7 +41,7 @@ TITLE = """
|
|
40 |
tracer = LangChainTracer(project_name=os.getenv('LANGCHAIN_PROJECT'))
|
41 |
|
42 |
config = RunnableConfig(
|
43 |
-
run_name = os.getenv('LANGCHAIN_RUNNAME', "ea4all-mcp"),
|
44 |
tags = [os.getenv('EA4ALL_ENV', "MCP")],
|
45 |
callbacks = [tracer],
|
46 |
recursion_limit = 25,
|
@@ -63,7 +64,7 @@ async def run_qna_agentic_system(question: str) -> AsyncGenerator[list, None]:
|
|
63 |
format_response = ""
|
64 |
chat_memory = []
|
65 |
if not question:
|
66 |
-
format_response = "Hi, how are you today? To start
|
67 |
chat_memory.append(ChatMessage(role="assistant", content=format_response))
|
68 |
yield chat_memory
|
69 |
|
@@ -505,7 +506,7 @@ with gr.Blocks(title="Your ArchitectGPT",fill_height=True, fill_width=True) as e
|
|
505 |
qna_prompt.submit(run_qna_agentic_system,[qna_prompt],ea4all_chatbot, api_name="landscape_answering_agent")
|
506 |
#qna_prompt.submit(lambda: "", None, [qna_prompt])
|
507 |
#ea4all_chatbot.like(fn=get_user_feedback)
|
508 |
-
|
509 |
|
510 |
#Execute Reference Architecture
|
511 |
dbr_run.click(run_reference_architecture_agentic_system,show_progress='full',inputs=[dbr_text],outputs=[togaf_vision,tabs_togaf,tabs_reference_architecture, architecture_runway, diagram_header, tab_diagram], api_name="togaf_blueprint_generation")
|
@@ -515,7 +516,7 @@ with gr.Blocks(title="Your ArchitectGPT",fill_height=True, fill_width=True) as e
|
|
515 |
vqa_prompt.submit(run_vqa_agentic_system,[vqa_prompt, vqa_image], ea4all_vqa, api_name="diagram_answering_agent")
|
516 |
|
517 |
#ea4all_vqa.like(fn=get_user_feedback)
|
518 |
-
vqa_examples.input(
|
519 |
|
520 |
#Invoke CrewAI PMO Agentic System
|
521 |
pmo_prompt.submit(run_pmo_agentic_system,[pmo_prompt],pmo_chatbot, api_name="architect_demand_agent")
|
|
|
25 |
|
26 |
from ea4all.utils.utils import (
|
27 |
UIUtils,
|
28 |
+
ea4all_agent_init, get_image, filter_page,
|
29 |
+
get_question_diagram_from_example,
|
30 |
)
|
31 |
|
32 |
TITLE = """
|
|
|
41 |
tracer = LangChainTracer(project_name=os.getenv('LANGCHAIN_PROJECT'))
|
42 |
|
43 |
config = RunnableConfig(
|
44 |
+
run_name = os.getenv('LANGCHAIN_RUNNAME', "ea4all-gradio-agent-mcp-hackathon-run"),
|
45 |
tags = [os.getenv('EA4ALL_ENV', "MCP")],
|
46 |
callbacks = [tracer],
|
47 |
recursion_limit = 25,
|
|
|
64 |
format_response = ""
|
65 |
chat_memory = []
|
66 |
if not question:
|
67 |
+
format_response = "Hi, how are you today? To start using the EA4ALL MCP Tool, provide the required Inputs!"
|
68 |
chat_memory.append(ChatMessage(role="assistant", content=format_response))
|
69 |
yield chat_memory
|
70 |
|
|
|
506 |
qna_prompt.submit(run_qna_agentic_system,[qna_prompt],ea4all_chatbot, api_name="landscape_answering_agent")
|
507 |
#qna_prompt.submit(lambda: "", None, [qna_prompt])
|
508 |
#ea4all_chatbot.like(fn=get_user_feedback)
|
509 |
+
qna_examples.input(lambda value: value, qna_examples, qna_prompt, show_api=False)
|
510 |
|
511 |
#Execute Reference Architecture
|
512 |
dbr_run.click(run_reference_architecture_agentic_system,show_progress='full',inputs=[dbr_text],outputs=[togaf_vision,tabs_togaf,tabs_reference_architecture, architecture_runway, diagram_header, tab_diagram], api_name="togaf_blueprint_generation")
|
|
|
516 |
vqa_prompt.submit(run_vqa_agentic_system,[vqa_prompt, vqa_image], ea4all_vqa, api_name="diagram_answering_agent")
|
517 |
|
518 |
#ea4all_vqa.like(fn=get_user_feedback)
|
519 |
+
vqa_examples.input(get_question_diagram_from_example, vqa_examples, outputs=[vqa_prompt, vqa_image], show_api=False)
|
520 |
|
521 |
#Invoke CrewAI PMO Agentic System
|
522 |
pmo_prompt.submit(run_pmo_agentic_system,[pmo_prompt],pmo_chatbot, api_name="architect_demand_agent")
|
ea4all/src/ea4all_apm/graph.py
CHANGED
@@ -6,8 +6,6 @@ and key functions for processing & routing user queries, generating answer to
|
|
6 |
Enterprise Architecture related user questions
|
7 |
about an IT Landscape or Websearch.
|
8 |
"""
|
9 |
-
import json
|
10 |
-
import tempfile
|
11 |
import os
|
12 |
|
13 |
from langgraph.graph import END, StateGraph
|
@@ -19,7 +17,7 @@ from langchain_core.prompts import PromptTemplate, FewShotChatMessagePromptTempl
|
|
19 |
from langchain_core.prompts import ChatPromptTemplate
|
20 |
from langchain_core.output_parsers.json import JsonOutputParser
|
21 |
from langchain_core.output_parsers import StrOutputParser
|
22 |
-
from langchain_core.runnables
|
23 |
from langchain_core.runnables import RunnablePassthrough, RunnableConfig
|
24 |
from langchain_core.runnables import RunnableGenerator
|
25 |
from langchain_core.documents import Document
|
@@ -27,19 +25,15 @@ from langchain_core.documents import Document
|
|
27 |
from langchain.load import dumps, loads
|
28 |
from langchain.hub import pull
|
29 |
|
30 |
-
##Utils and tools
|
31 |
-
from langchain_community.document_loaders import JSONLoader
|
32 |
-
from langchain_community.utilities import BingSearchAPIWrapper
|
33 |
-
from langchain_community.tools.bing_search.tool import BingSearchResults
|
34 |
-
|
35 |
from operator import itemgetter
|
|
|
36 |
|
37 |
#compute amount of tokens used
|
38 |
import tiktoken
|
39 |
|
40 |
#import APMGraph packages
|
41 |
from ea4all.src.ea4all_apm.configuration import AgentConfiguration
|
42 |
-
from ea4all.src.ea4all_apm.state import
|
43 |
import ea4all.src.ea4all_apm.prompts as e4p
|
44 |
from ea4all.src.shared.utils import (
|
45 |
load_mock_content,
|
@@ -50,12 +44,15 @@ from ea4all.src.shared.utils import (
|
|
50 |
_join_paths,
|
51 |
)
|
52 |
from ea4all.src.shared import vectorstore
|
|
|
|
|
|
|
53 |
|
54 |
# This file contains sample APM QUESTIONS
|
55 |
APM_MOCK_QNA = "apm_qna_mock.txt"
|
56 |
|
57 |
async def retrieve_documents(
|
58 |
-
state:
|
59 |
) -> dict[str, list[Document]]:
|
60 |
"""Retrieve documents based on a given query.
|
61 |
|
@@ -223,6 +220,7 @@ async def get_retrieval_chain(rag_input, ea4all_user, question, retriever, confi
|
|
223 |
decomposition_prompt = ChatPromptTemplate.from_template(e4p.decomposition_answer_recursevely_template)
|
224 |
|
225 |
# Answer each question and return final answer
|
|
|
226 |
q_a_pairs = ""
|
227 |
for q in questions:
|
228 |
rag_chain = (
|
@@ -257,7 +255,7 @@ async def get_retrieval_chain(rag_input, ea4all_user, question, retriever, confi
|
|
257 |
retrieval_chain = (
|
258 |
{
|
259 |
# Retrieve context using the normal question
|
260 |
-
"normal_context": RunnableLambda(lambda x: x
|
261 |
# Retrieve context using the step-back question
|
262 |
"step_back_context": generate_queries_step_back | retriever,
|
263 |
# Pass on the question
|
@@ -287,14 +285,14 @@ async def get_retrieval_chain(rag_input, ea4all_user, question, retriever, confi
|
|
287 |
|
288 |
#Get relevant asnwers to user query
|
289 |
##get_relevant_documents "deprecated" - replaced by invoke : 2024-06-07
|
290 |
-
def get_relevant_answers(query, config: RunnableConfig):
|
291 |
|
292 |
if query != "":
|
293 |
#retriever.vectorstore.index.ntotal
|
294 |
#retriever = retriever_faiss(user_ip)
|
295 |
#response = retriever.invoke({"standalone_question": query})
|
296 |
|
297 |
-
response = retrieve_documents(
|
298 |
return response
|
299 |
else:
|
300 |
return []
|
@@ -346,7 +344,7 @@ def get_relevant_questions():
|
|
346 |
|
347 |
#Rephrase the original user question based on system prompt to lead a better LLM answer
|
348 |
def user_query_rephrasing(
|
349 |
-
state:
|
350 |
) -> dict[str,str]:
|
351 |
|
352 |
question = getattr(state,'question')
|
@@ -475,7 +473,7 @@ async def grade_documents(state, config: RunnableConfig):
|
|
475 |
score = retrieval_grader(llm).ainvoke(
|
476 |
{"user_question": question, "document": d.page_content}
|
477 |
)
|
478 |
-
grade = score
|
479 |
# Document relevant
|
480 |
if grade.lower() == "yes":
|
481 |
print("---GRADE: DOCUMENT RELEVANT---")
|
@@ -518,7 +516,7 @@ def decide_to_generate(state):
|
|
518 |
return "generate"
|
519 |
|
520 |
def grade_generation_v_documents_and_question(
|
521 |
-
state:
|
522 |
"""
|
523 |
Determines whether the generation is grounded in the document and answers question.
|
524 |
|
@@ -571,7 +569,7 @@ def grade_generation_v_documents_and_question(
|
|
571 |
yield "not supported"
|
572 |
|
573 |
async def apm_query_router(
|
574 |
-
state:
|
575 |
) -> str:
|
576 |
|
577 |
configuration = AgentConfiguration.from_runnable_config(config)
|
@@ -595,12 +593,16 @@ async def apm_query_router(
|
|
595 |
|
596 |
response = await route.ainvoke({"user_question": user_query})
|
597 |
|
598 |
-
|
599 |
-
|
|
|
|
|
|
|
|
|
600 |
return datasource
|
601 |
|
602 |
async def retrieve(
|
603 |
-
state:
|
604 |
):
|
605 |
"""
|
606 |
Retrieve documents
|
@@ -654,49 +656,8 @@ async def retrieve(
|
|
654 |
|
655 |
return {"documents": format_docs(documents['cdocs']), "question": question, "rag":getattr(state,'rag')}
|
656 |
|
657 |
-
async def websearch(
|
658 |
-
state: APMState, config: RunnableConfig
|
659 |
-
) -> dict[str,any]:
|
660 |
-
"""
|
661 |
-
Web search based on the re-phrased question.
|
662 |
-
|
663 |
-
Args:
|
664 |
-
state (dict): The current graph state
|
665 |
-
config (RunnableConfig): Configuration with the model used for query analysis.
|
666 |
-
|
667 |
-
Returns:
|
668 |
-
state (dict): Updates documents key with appended web results
|
669 |
-
"""
|
670 |
-
|
671 |
-
# print("---WEB SEARCH---")
|
672 |
-
##Rephrase user question to lead bettern LLM response
|
673 |
-
question = user_query_rephrasing(state=state, config=config)['question']
|
674 |
-
|
675 |
-
##API Wrapper
|
676 |
-
search = BingSearchAPIWrapper()
|
677 |
-
|
678 |
-
##Bing Search Results
|
679 |
-
web_results = BingSearchResults(k=3, api_wrapper=search)
|
680 |
-
result = await web_results.ainvoke(
|
681 |
-
{"query": question},
|
682 |
-
)
|
683 |
-
fixed_string = result.replace("'", "\"")
|
684 |
-
result_json = json.loads(fixed_string)
|
685 |
-
|
686 |
-
# Create a temporary file
|
687 |
-
with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_file:
|
688 |
-
# Write the JSON data to the temporary file
|
689 |
-
json.dump(result_json, temp_file)
|
690 |
-
temp_file.flush()
|
691 |
-
|
692 |
-
# Load the JSON data from the temporary file
|
693 |
-
loader = JSONLoader(file_path=temp_file.name, jq_schema=".[]", text_content=False)
|
694 |
-
docs = loader.load()
|
695 |
-
|
696 |
-
return {"documents": format_docs(docs), "question": question, "web_search": "Yes", "generation": None}
|
697 |
-
|
698 |
### Edges ###
|
699 |
-
def route_to_node(state:
|
700 |
|
701 |
if state.source == "websearch":
|
702 |
#print("---ROUTE QUESTION TO WEB SEARCH---")
|
@@ -706,8 +667,8 @@ def route_to_node(state:APMState):
|
|
706 |
return "vectorstore"
|
707 |
|
708 |
async def route_question(
|
709 |
-
state:
|
710 |
-
) -> dict[str,
|
711 |
"""
|
712 |
Route question to web search or RAG.
|
713 |
|
@@ -724,12 +685,16 @@ async def route_question(
|
|
724 |
return {"source":source}
|
725 |
|
726 |
async def stream_generation(
|
727 |
-
state:
|
728 |
-
):
|
729 |
configuration = AgentConfiguration.from_runnable_config(config)
|
730 |
|
731 |
llm = get_llm_client(model=configuration.query_model, api_base_url=configuration.api_base_url,streaming=configuration.streaming)
|
732 |
|
|
|
|
|
|
|
|
|
733 |
async for s in state:
|
734 |
documents = getattr(s,"documents")
|
735 |
web_search = getattr(s,"web_search")
|
@@ -759,8 +724,8 @@ async def stream_generation(
|
|
759 |
yield(output)
|
760 |
|
761 |
async def generate(
|
762 |
-
state:
|
763 |
-
) -> dict[str,
|
764 |
"""
|
765 |
Generate answer
|
766 |
|
@@ -797,7 +762,7 @@ async def generate(
|
|
797 |
|
798 |
#ea4all-qna-agent-conversational-with-memory
|
799 |
async def apm_agentic_qna(
|
800 |
-
state:
|
801 |
|
802 |
configuration = AgentConfiguration.from_runnable_config(config)
|
803 |
|
@@ -845,14 +810,17 @@ async def apm_agentic_qna(
|
|
845 |
|
846 |
return {"documents": format_docs(documents['cdocs']), "question": question, "rag":5, "web_search": "No", "generation": None}
|
847 |
|
848 |
-
async def final(state:
|
849 |
return {"safety_status": state}
|
850 |
|
851 |
-
async def choose_next(state:
|
852 |
-
|
|
|
|
|
|
|
853 |
|
854 |
class SafetyCheck:
|
855 |
-
def apm_safety_check(self,state:
|
856 |
|
857 |
configuration = AgentConfiguration.from_runnable_config(config)
|
858 |
question = state.question
|
@@ -877,7 +845,7 @@ class SafetyCheck:
|
|
877 |
def __init__(self):
|
878 |
self._safety_run = self.apm_safety_check
|
879 |
|
880 |
-
def __call__(self, state:
|
881 |
try:
|
882 |
response = getattr(self, '_safety_run')(state, config)
|
883 |
return {"safety_status": [response['safety_status'][0], "", state.question]}
|
@@ -886,7 +854,7 @@ class SafetyCheck:
|
|
886 |
|
887 |
##BUILD APM Graph
|
888 |
# Build graph
|
889 |
-
workflow = StateGraph(
|
890 |
|
891 |
# Define the nodes
|
892 |
workflow.add_node("safety_check",SafetyCheck())
|
|
|
6 |
Enterprise Architecture related user questions
|
7 |
about an IT Landscape or Websearch.
|
8 |
"""
|
|
|
|
|
9 |
import os
|
10 |
|
11 |
from langgraph.graph import END, StateGraph
|
|
|
17 |
from langchain_core.prompts import ChatPromptTemplate
|
18 |
from langchain_core.output_parsers.json import JsonOutputParser
|
19 |
from langchain_core.output_parsers import StrOutputParser
|
20 |
+
from langchain_core.runnables import RunnableLambda
|
21 |
from langchain_core.runnables import RunnablePassthrough, RunnableConfig
|
22 |
from langchain_core.runnables import RunnableGenerator
|
23 |
from langchain_core.documents import Document
|
|
|
25 |
from langchain.load import dumps, loads
|
26 |
from langchain.hub import pull
|
27 |
|
|
|
|
|
|
|
|
|
|
|
28 |
from operator import itemgetter
|
29 |
+
from typing import AsyncGenerator, AsyncIterator
|
30 |
|
31 |
#compute amount of tokens used
|
32 |
import tiktoken
|
33 |
|
34 |
#import APMGraph packages
|
35 |
from ea4all.src.ea4all_apm.configuration import AgentConfiguration
|
36 |
+
from ea4all.src.ea4all_apm.state import InputState, OutputState, OverallState
|
37 |
import ea4all.src.ea4all_apm.prompts as e4p
|
38 |
from ea4all.src.shared.utils import (
|
39 |
load_mock_content,
|
|
|
44 |
_join_paths,
|
45 |
)
|
46 |
from ea4all.src.shared import vectorstore
|
47 |
+
from ea4all.src.tools.tools import (
|
48 |
+
websearch,
|
49 |
+
)
|
50 |
|
51 |
# This file contains sample APM QUESTIONS
|
52 |
APM_MOCK_QNA = "apm_qna_mock.txt"
|
53 |
|
54 |
async def retrieve_documents(
|
55 |
+
state: OverallState, *, config: RunnableConfig
|
56 |
) -> dict[str, list[Document]]:
|
57 |
"""Retrieve documents based on a given query.
|
58 |
|
|
|
220 |
decomposition_prompt = ChatPromptTemplate.from_template(e4p.decomposition_answer_recursevely_template)
|
221 |
|
222 |
# Answer each question and return final answer
|
223 |
+
answer = ""
|
224 |
q_a_pairs = ""
|
225 |
for q in questions:
|
226 |
rag_chain = (
|
|
|
255 |
retrieval_chain = (
|
256 |
{
|
257 |
# Retrieve context using the normal question
|
258 |
+
"normal_context": RunnableLambda(lambda x: getattr(x, "standalone_question")) | retriever,
|
259 |
# Retrieve context using the step-back question
|
260 |
"step_back_context": generate_queries_step_back | retriever,
|
261 |
# Pass on the question
|
|
|
285 |
|
286 |
#Get relevant asnwers to user query
|
287 |
##get_relevant_documents "deprecated" - replaced by invoke : 2024-06-07
|
288 |
+
async def get_relevant_answers(state: OverallState, query, config: RunnableConfig):
|
289 |
|
290 |
if query != "":
|
291 |
#retriever.vectorstore.index.ntotal
|
292 |
#retriever = retriever_faiss(user_ip)
|
293 |
#response = retriever.invoke({"standalone_question": query})
|
294 |
|
295 |
+
response = await retrieve_documents(state, config=config)
|
296 |
return response
|
297 |
else:
|
298 |
return []
|
|
|
344 |
|
345 |
#Rephrase the original user question based on system prompt to lead a better LLM answer
|
346 |
def user_query_rephrasing(
|
347 |
+
state: OverallState, _prompt=None, *, config: RunnableConfig
|
348 |
) -> dict[str,str]:
|
349 |
|
350 |
question = getattr(state,'question')
|
|
|
473 |
score = retrieval_grader(llm).ainvoke(
|
474 |
{"user_question": question, "document": d.page_content}
|
475 |
)
|
476 |
+
grade = getattr(score,"score", "no")
|
477 |
# Document relevant
|
478 |
if grade.lower() == "yes":
|
479 |
print("---GRADE: DOCUMENT RELEVANT---")
|
|
|
516 |
return "generate"
|
517 |
|
518 |
def grade_generation_v_documents_and_question(
|
519 |
+
state:OverallState, config: RunnableConfig):
|
520 |
"""
|
521 |
Determines whether the generation is grounded in the document and answers question.
|
522 |
|
|
|
569 |
yield "not supported"
|
570 |
|
571 |
async def apm_query_router(
|
572 |
+
state: OverallState, config: RunnableConfig
|
573 |
) -> str:
|
574 |
|
575 |
configuration = AgentConfiguration.from_runnable_config(config)
|
|
|
593 |
|
594 |
response = await route.ainvoke({"user_question": user_query})
|
595 |
|
596 |
+
extracted = extract_structured_output(response.content)
|
597 |
+
if extracted is not None:
|
598 |
+
datasource = extracted.get('datasource', 'vectorstore')
|
599 |
+
else:
|
600 |
+
datasource = 'vectorstore'
|
601 |
+
|
602 |
return datasource
|
603 |
|
604 |
async def retrieve(
|
605 |
+
state: OverallState, config: RunnableConfig
|
606 |
):
|
607 |
"""
|
608 |
Retrieve documents
|
|
|
656 |
|
657 |
return {"documents": format_docs(documents['cdocs']), "question": question, "rag":getattr(state,'rag')}
|
658 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
659 |
### Edges ###
|
660 |
+
def route_to_node(state:OverallState):
|
661 |
|
662 |
if state.source == "websearch":
|
663 |
#print("---ROUTE QUESTION TO WEB SEARCH---")
|
|
|
667 |
return "vectorstore"
|
668 |
|
669 |
async def route_question(
|
670 |
+
state: OverallState, config: RunnableConfig
|
671 |
+
) -> dict[str, str]:
|
672 |
"""
|
673 |
Route question to web search or RAG.
|
674 |
|
|
|
685 |
return {"source":source}
|
686 |
|
687 |
async def stream_generation(
|
688 |
+
state: OverallState, config: RunnableConfig
|
689 |
+
) -> AsyncGenerator[str, None]:
|
690 |
configuration = AgentConfiguration.from_runnable_config(config)
|
691 |
|
692 |
llm = get_llm_client(model=configuration.query_model, api_base_url=configuration.api_base_url,streaming=configuration.streaming)
|
693 |
|
694 |
+
documents = None
|
695 |
+
web_search = None
|
696 |
+
question = None
|
697 |
+
chat_memory = None
|
698 |
async for s in state:
|
699 |
documents = getattr(s,"documents")
|
700 |
web_search = getattr(s,"web_search")
|
|
|
724 |
yield(output)
|
725 |
|
726 |
async def generate(
|
727 |
+
state: OverallState, config: RunnableConfig
|
728 |
+
) -> dict[str, str]:
|
729 |
"""
|
730 |
Generate answer
|
731 |
|
|
|
762 |
|
763 |
#ea4all-qna-agent-conversational-with-memory
|
764 |
async def apm_agentic_qna(
|
765 |
+
state:OverallState, config: RunnableConfig):
|
766 |
|
767 |
configuration = AgentConfiguration.from_runnable_config(config)
|
768 |
|
|
|
810 |
|
811 |
return {"documents": format_docs(documents['cdocs']), "question": question, "rag":5, "web_search": "No", "generation": None}
|
812 |
|
813 |
+
async def final(state: OverallState):
|
814 |
return {"safety_status": state}
|
815 |
|
816 |
+
async def choose_next(state: OverallState):
|
817 |
+
if state.safety_status is not None and len(state.safety_status) > 0 and state.safety_status[0] == 'no':
|
818 |
+
return "exit"
|
819 |
+
else:
|
820 |
+
return "route"
|
821 |
|
822 |
class SafetyCheck:
|
823 |
+
def apm_safety_check(self,state: OverallState, config: RunnableConfig):
|
824 |
|
825 |
configuration = AgentConfiguration.from_runnable_config(config)
|
826 |
question = state.question
|
|
|
845 |
def __init__(self):
|
846 |
self._safety_run = self.apm_safety_check
|
847 |
|
848 |
+
def __call__(self, state: OverallState, config: RunnableConfig) -> dict[str, list]:
|
849 |
try:
|
850 |
response = getattr(self, '_safety_run')(state, config)
|
851 |
return {"safety_status": [response['safety_status'][0], "", state.question]}
|
|
|
854 |
|
855 |
##BUILD APM Graph
|
856 |
# Build graph
|
857 |
+
workflow = StateGraph(OverallState, input=InputState, output=OutputState, config_schema=AgentConfiguration)
|
858 |
|
859 |
# Define the nodes
|
860 |
workflow.add_node("safety_check",SafetyCheck())
|
ea4all/src/ea4all_apm/state.py
CHANGED
@@ -8,6 +8,11 @@ from dataclasses import dataclass, field
|
|
8 |
from typing import Optional, Literal, List, Tuple
|
9 |
from typing_extensions import TypedDict
|
10 |
|
|
|
|
|
|
|
|
|
|
|
11 |
# Optional, the InputState is a restricted version of the State that is used to
|
12 |
# define a narrower interface to the outside world vs. what is maintained
|
13 |
# internally.
|
@@ -25,58 +30,31 @@ class InputState:
|
|
25 |
question: user question
|
26 |
"""
|
27 |
question: str
|
28 |
-
safety_status: Optional[Tuple[str, str, str]] = None
|
29 |
-
|
30 |
-
"""Messages track the primary execution state of the agent.
|
31 |
-
|
32 |
-
Typically accumulates a pattern of Human/AI/Human/AI messages; if
|
33 |
-
you were to combine this template with a tool-calling ReAct agent pattern,
|
34 |
-
it may look like this:
|
35 |
-
|
36 |
-
1. HumanMessage - user input
|
37 |
-
2. AIMessage with .tool_calls - agent picking tool(s) to use to collect
|
38 |
-
information
|
39 |
-
3. ToolMessage(s) - the responses (or errors) from the executed tools
|
40 |
-
|
41 |
-
(... repeat steps 2 and 3 as needed ...)
|
42 |
-
4. AIMessage without .tool_calls - agent responding in unstructured
|
43 |
-
format to the user.
|
44 |
-
|
45 |
-
5. HumanMessage - user responds with the next conversational turn.
|
46 |
-
|
47 |
-
(... repeat steps 2-5 as needed ... )
|
48 |
-
|
49 |
-
Merges two lists of messages, updating existing messages by ID.
|
50 |
-
|
51 |
-
By default, this ensures the state is "append-only", unless the
|
52 |
-
new message has the same ID as an existing message.
|
53 |
-
|
54 |
-
Returns:
|
55 |
-
A new list of messages with the messages from `right` merged into `left`.
|
56 |
-
If a message in `right` has the same ID as a message in `left`, the
|
57 |
-
message from `right` will replace the message from `left`."""
|
58 |
|
|
|
|
|
|
|
|
|
59 |
|
60 |
-
|
61 |
-
"""
|
62 |
-
logic: str
|
63 |
-
datasource: Optional[Literal["vectorstore", "websearch"]] = None
|
64 |
|
65 |
@dataclass(kw_only=True)
|
66 |
-
class
|
67 |
"""State of the APM graph / agent."""
|
68 |
|
69 |
"""
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
"""
|
|
|
80 |
router: Optional[Router] = None
|
81 |
source: Optional[str] = None
|
82 |
rag: Optional[str] = None
|
|
|
8 |
from typing import Optional, Literal, List, Tuple
|
9 |
from typing_extensions import TypedDict
|
10 |
|
11 |
+
class Router(TypedDict):
|
12 |
+
"""Classify a user query."""
|
13 |
+
logic: str
|
14 |
+
datasource: Optional[Literal["vectorstore", "websearch"]]
|
15 |
+
|
16 |
# Optional, the InputState is a restricted version of the State that is used to
|
17 |
# define a narrower interface to the outside world vs. what is maintained
|
18 |
# internally.
|
|
|
30 |
question: user question
|
31 |
"""
|
32 |
question: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
+
@dataclass(kw_only=True)
|
35 |
+
class OutputState:
|
36 |
+
"""Represents the output schema for the APM agent.
|
37 |
+
"""
|
38 |
|
39 |
+
answer: str
|
40 |
+
"""Answer to user's Architecture IT Landscape question about ."""
|
|
|
|
|
41 |
|
42 |
@dataclass(kw_only=True)
|
43 |
+
class OverallState(InputState):
|
44 |
"""State of the APM graph / agent."""
|
45 |
|
46 |
"""
|
47 |
+
safety_status: user question's safeguarding status, justification, rephrased question
|
48 |
+
router: classification of the user's query
|
49 |
+
source: RAG or websearch
|
50 |
+
web_search: whether to add search
|
51 |
+
retrieved: list of documents retrieved by the retriever
|
52 |
+
rag: last RAG approach used
|
53 |
+
chat_memory: user chat memory
|
54 |
+
generation: should the agent generate a response
|
55 |
+
documents: list of documents retrieved by the retriever
|
56 |
"""
|
57 |
+
safety_status: Optional[Tuple[str, str, str]] = None
|
58 |
router: Optional[Router] = None
|
59 |
source: Optional[str] = None
|
60 |
rag: Optional[str] = None
|
ea4all/src/graph.py
CHANGED
@@ -83,7 +83,7 @@ def make_supervisor_node(model: BaseChatModel, members: list[str]):
|
|
83 |
if _goto not in ["portfolio_team", "diagram_team", "blueprint_team", "websearch_team"]:
|
84 |
_goto = "__end__"
|
85 |
|
86 |
-
print(f"---Supervisor got a request---
|
87 |
|
88 |
return Command(
|
89 |
#update={"next": _goto},
|
@@ -98,7 +98,7 @@ async def call_landscape_agentic(state: State, config: RunnableConfig) -> Comman
|
|
98 |
update={
|
99 |
"messages": [
|
100 |
AIMessage(
|
101 |
-
content=
|
102 |
)
|
103 |
]
|
104 |
},
|
@@ -151,7 +151,7 @@ async def call_togaf_agentic(state: State, config: RunnableConfig) -> Command[Li
|
|
151 |
|
152 |
# Wrap-up websearch answer to user's question
|
153 |
async def call_generate_websearch(state:State, config: RunnableConfig) -> Command[Literal["__end__"]]:
|
154 |
-
from ea4all.src.ea4all_apm.state import
|
155 |
|
156 |
if config is not None:
|
157 |
source = config.get('metadata', {}).get('langgraph_node', 'unknown')
|
@@ -164,7 +164,7 @@ async def call_generate_websearch(state:State, config: RunnableConfig) -> Comman
|
|
164 |
"source": source
|
165 |
}
|
166 |
|
167 |
-
apm_state =
|
168 |
generation = await apm_graph.nodes["generate"].ainvoke(apm_state, config)
|
169 |
|
170 |
return Command(
|
|
|
83 |
if _goto not in ["portfolio_team", "diagram_team", "blueprint_team", "websearch_team"]:
|
84 |
_goto = "__end__"
|
85 |
|
86 |
+
print(f"---Supervisor got a request--- Question: {state['messages'][-1].content} ==> Routing to {_goto}\n")
|
87 |
|
88 |
return Command(
|
89 |
#update={"next": _goto},
|
|
|
98 |
update={
|
99 |
"messages": [
|
100 |
AIMessage(
|
101 |
+
content=str(response), name="landscape_agentic"
|
102 |
)
|
103 |
]
|
104 |
},
|
|
|
151 |
|
152 |
# Wrap-up websearch answer to user's question
|
153 |
async def call_generate_websearch(state:State, config: RunnableConfig) -> Command[Literal["__end__"]]:
|
154 |
+
from ea4all.src.ea4all_apm.state import OverallState
|
155 |
|
156 |
if config is not None:
|
157 |
source = config.get('metadata', {}).get('langgraph_node', 'unknown')
|
|
|
164 |
"source": source
|
165 |
}
|
166 |
|
167 |
+
apm_state = OverallState(**state_dict)
|
168 |
generation = await apm_graph.nodes["generate"].ainvoke(apm_state, config)
|
169 |
|
170 |
return Command(
|
ea4all/src/tools/tools.py
CHANGED
@@ -2,8 +2,9 @@ from typing import Literal, Annotated
|
|
2 |
from typing_extensions import TypedDict
|
3 |
import json
|
4 |
import tempfile
|
|
|
5 |
|
6 |
-
from langchain_core.runnables import RunnableConfig
|
7 |
|
8 |
from langgraph.graph import END
|
9 |
from langgraph.types import Command
|
@@ -21,14 +22,14 @@ from ea4all.src.shared.configuration import (
|
|
21 |
|
22 |
from ea4all.src.shared.state import (
|
23 |
State
|
24 |
-
)
|
25 |
|
26 |
from ea4all.src.shared.utils import (
|
27 |
get_llm_client,
|
28 |
format_docs,
|
29 |
)
|
30 |
|
31 |
-
def make_supervisor_node(config: RunnableConfig, members: list[str]) ->
|
32 |
options = ["FINISH"] + members
|
33 |
system_prompt = (
|
34 |
"You are a supervisor tasked with managing a conversation between the"
|
@@ -61,9 +62,9 @@ def make_supervisor_node(config: RunnableConfig, members: list[str]) -> str:
|
|
61 |
|
62 |
return Command(goto=goto, update={"next": goto})
|
63 |
|
64 |
-
return supervisor_node
|
65 |
|
66 |
-
async def websearch(state: State):
|
67 |
"""
|
68 |
Web search based on the re-phrased question.
|
69 |
|
@@ -76,15 +77,20 @@ async def websearch(state: State):
|
|
76 |
"""
|
77 |
|
78 |
##API Wrapper
|
79 |
-
|
|
|
|
|
|
|
|
|
|
|
80 |
|
81 |
question = state.get('messages')[-1].content
|
82 |
|
83 |
##Bing Search Results
|
84 |
web_results = BingSearchResults(
|
85 |
-
k=5,
|
86 |
api_wrapper=search,
|
87 |
handle_tool_error=True,
|
|
|
88 |
)
|
89 |
|
90 |
result = await web_results.ainvoke({"query": question})
|
|
|
2 |
from typing_extensions import TypedDict
|
3 |
import json
|
4 |
import tempfile
|
5 |
+
import os
|
6 |
|
7 |
+
from langchain_core.runnables import RunnableLambda, RunnableConfig
|
8 |
|
9 |
from langgraph.graph import END
|
10 |
from langgraph.types import Command
|
|
|
22 |
|
23 |
from ea4all.src.shared.state import (
|
24 |
State
|
25 |
+
)
|
26 |
|
27 |
from ea4all.src.shared.utils import (
|
28 |
get_llm_client,
|
29 |
format_docs,
|
30 |
)
|
31 |
|
32 |
+
def make_supervisor_node(config: RunnableConfig, members: list[str]) -> RunnableLambda:
|
33 |
options = ["FINISH"] + members
|
34 |
system_prompt = (
|
35 |
"You are a supervisor tasked with managing a conversation between the"
|
|
|
62 |
|
63 |
return Command(goto=goto, update={"next": goto})
|
64 |
|
65 |
+
return RunnableLambda(supervisor_node)
|
66 |
|
67 |
+
async def websearch(state: State) -> dict[str,dict[str,str]]:
|
68 |
"""
|
69 |
Web search based on the re-phrased question.
|
70 |
|
|
|
77 |
"""
|
78 |
|
79 |
##API Wrapper
|
80 |
+
bing_subscription_key = os.environ.get("BING_SUBSCRIPTION_KEY", "")
|
81 |
+
bing_search_url = os.environ.get("BING_SEARCH_URL", "https://api.bing.microsoft.com/v7.0/search")
|
82 |
+
search = BingSearchAPIWrapper(
|
83 |
+
bing_subscription_key=bing_subscription_key,
|
84 |
+
bing_search_url=bing_search_url
|
85 |
+
)
|
86 |
|
87 |
question = state.get('messages')[-1].content
|
88 |
|
89 |
##Bing Search Results
|
90 |
web_results = BingSearchResults(
|
|
|
91 |
api_wrapper=search,
|
92 |
handle_tool_error=True,
|
93 |
+
args_schema={"k":"5"},
|
94 |
)
|
95 |
|
96 |
result = await web_results.ainvoke({"query": question})
|
ea4all/utils/utils.py
CHANGED
@@ -8,7 +8,6 @@ from ea4all.src.shared.configuration import BaseConfiguration
|
|
8 |
from ea4all.src.ea4all_indexer.configuration import IndexConfiguration
|
9 |
from ea4all.src.ea4all_indexer.graph import indexer_graph
|
10 |
|
11 |
-
|
12 |
from langchain_community.document_loaders import ConfluenceLoader
|
13 |
from langchain_core.messages import ChatMessage
|
14 |
from langsmith import Client
|
@@ -164,3 +163,11 @@ def on_dbrtext(file):
|
|
164 |
|
165 |
def unload_dbr():
|
166 |
return gr.TextArea(visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
from ea4all.src.ea4all_indexer.configuration import IndexConfiguration
|
9 |
from ea4all.src.ea4all_indexer.graph import indexer_graph
|
10 |
|
|
|
11 |
from langchain_community.document_loaders import ConfluenceLoader
|
12 |
from langchain_core.messages import ChatMessage
|
13 |
from langsmith import Client
|
|
|
163 |
|
164 |
def unload_dbr():
|
165 |
return gr.TextArea(visible=False)
|
166 |
+
|
167 |
+
def get_question_diagram_from_example(value) -> list:
|
168 |
+
"""
|
169 |
+
Extracts the question and diagram from the selected example.
|
170 |
+
"""
|
171 |
+
if value:
|
172 |
+
return [value['text'], value['files'][-1]] if 'files' in value else [value['text'], None]
|
173 |
+
return ["", None]
|
pyproject.toml
CHANGED
@@ -4,4 +4,6 @@ version = "0.1.0"
|
|
4 |
description = "EA4ALL Agentic System MCP Server"
|
5 |
readme = "README.md"
|
6 |
requires-python = ">=3.12"
|
7 |
-
dependencies = [
|
|
|
|
|
|
4 |
description = "EA4ALL Agentic System MCP Server"
|
5 |
readme = "README.md"
|
6 |
requires-python = ">=3.12"
|
7 |
+
dependencies = [
|
8 |
+
"jq>=1.8.0",
|
9 |
+
]
|
requirements.txt
CHANGED
@@ -4,6 +4,7 @@ gradio==5.32.1
|
|
4 |
gradio_client==1.10.2
|
5 |
graphviz
|
6 |
huggingface-hub
|
|
|
7 |
langchain==0.3.25
|
8 |
langchain-community==0.3.24
|
9 |
langchain-core==0.3.61
|
|
|
4 |
gradio_client==1.10.2
|
5 |
graphviz
|
6 |
huggingface-hub
|
7 |
+
jq>=1.8.0
|
8 |
langchain==0.3.25
|
9 |
langchain-community==0.3.24
|
10 |
langchain-core==0.3.61
|
uv.lock
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version = 1
|
2 |
+
revision = 2
|
3 |
+
requires-python = ">=3.12"
|
4 |
+
|
5 |
+
[[package]]
|
6 |
+
name = "ea4all-gradio-agent-mcp-hackathon"
|
7 |
+
version = "0.1.0"
|
8 |
+
source = { virtual = "." }
|
9 |
+
dependencies = [
|
10 |
+
{ name = "jq" },
|
11 |
+
]
|
12 |
+
|
13 |
+
[package.metadata]
|
14 |
+
requires-dist = [{ name = "jq", specifier = ">=1.8.0" }]
|
15 |
+
|
16 |
+
[[package]]
|
17 |
+
name = "jq"
|
18 |
+
version = "1.8.0"
|
19 |
+
source = { registry = "https://pypi.org/simple" }
|
20 |
+
sdist = { url = "https://files.pythonhosted.org/packages/ba/32/3eaca3ac81c804d6849da2e9f536ac200f4ad46a696890854c1f73b2f749/jq-1.8.0.tar.gz", hash = "sha256:53141eebca4bf8b4f2da5e44271a8a3694220dfd22d2b4b2cfb4816b2b6c9057", size = 2058265, upload-time = "2024-08-17T08:13:36.301Z" }
|
21 |
+
wheels = [
|
22 |
+
{ url = "https://files.pythonhosted.org/packages/45/b3/dd0d41cecb0d8712bc792b3c40b42a36c355d814d61f6bda4d61cbb188e5/jq-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:14f5988ae3604ebfdba2da398f9bd941bb3a72144a2831cfec2bc22bd23d5563", size = 415943, upload-time = "2024-08-17T08:14:38.437Z" },
|
23 |
+
{ url = "https://files.pythonhosted.org/packages/9b/2c/39df803632c7222e9cd6922101966ddbec05d1c4213e7923c95e4e442666/jq-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f8903b66fac9f46de72b3a2f69bfa3c638a7a8d52610d1894df87ef0a9e4d2d3", size = 422267, upload-time = "2024-08-17T08:14:40.746Z" },
|
24 |
+
{ url = "https://files.pythonhosted.org/packages/3a/b3/ddc1e691b832c6aa0f5142935099c1f05a89ff2f337201e2dcfafc726ec9/jq-1.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccda466f5722fa9be789099ce253bfc177e49f9a981cb7f5b6369ea37041104", size = 729142, upload-time = "2024-08-17T08:14:44.144Z" },
|
25 |
+
{ url = "https://files.pythonhosted.org/packages/c5/b9/42a55d08397d25b4b1f6580f58c59ba3e3e120270db2e75923644ccc0d29/jq-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f57649e84a09b334eeb80d22ecc96ff7b31701f3f818ef14cb8bb162c84863", size = 748871, upload-time = "2024-08-17T08:14:46.816Z" },
|
26 |
+
{ url = "https://files.pythonhosted.org/packages/90/4f/83639fdae641b7e8095b4a51d87a3da46737e70570d9df14d99ea15a0b16/jq-1.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7453731008eb7671725222781eb7bc5ed96e80fc9a652d177cb982276d3e08b4", size = 735908, upload-time = "2024-08-17T08:14:48.865Z" },
|
27 |
+
{ url = "https://files.pythonhosted.org/packages/f7/9f/f54c2050b21490201613a7328534d2cb0c34e5a547167849a1464d89ae3e/jq-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:917812663613fc0542117bbe7ec43c8733b0c6bb174db6be06a15fc612de3b70", size = 721970, upload-time = "2024-08-17T08:14:51.442Z" },
|
28 |
+
{ url = "https://files.pythonhosted.org/packages/24/b0/6c9a14ef103df4208e032bce25e66293201dacac18689d2ec4c0e68c8b77/jq-1.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ec9e4db978237470e9d65f747eb459f4ffee576c9c9f8ca92ab32d5687a46e4a", size = 746825, upload-time = "2024-08-17T08:14:53.536Z" },
|
29 |
+
{ url = "https://files.pythonhosted.org/packages/f4/67/4eb836a9eac5f02983ed7caf76c4d0cad32fdd6ae08176be892b3a6b3d17/jq-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9f2548c83473bbe88a32a0735cb949a5d01804f8d411efae5342b5d23be8a2f", size = 751186, upload-time = "2024-08-17T08:14:57.32Z" },
|
30 |
+
{ url = "https://files.pythonhosted.org/packages/2c/8f/66739f56ee1e3d144e7eef6453c5967275f75bf216e1915cdd9652a779aa/jq-1.8.0-cp312-cp312-win32.whl", hash = "sha256:e3da3538549d5bdc84e6282555be4ba5a50c3792db7d8d72d064cc6f48a2f722", size = 405483, upload-time = "2024-08-17T08:15:00.532Z" },
|
31 |
+
{ url = "https://files.pythonhosted.org/packages/f6/9f/e886c23b466fc41f105b715724c19dd6089585f2e34375f07c38c69ceaf1/jq-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:049ba2978e61e593299edc6dd57b9cefd680272740ad1d4703f8784f5fab644d", size = 417281, upload-time = "2024-08-17T08:15:03.048Z" },
|
32 |
+
{ url = "https://files.pythonhosted.org/packages/9c/25/c73afa16aedee3ae87b2e8ffb2d12bdb9c7a34a8c9ab5038318cb0b431fe/jq-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aea6161c4d975230e85735c0214c386e66035e96cfc4fd69159e87f46c09d4", size = 415000, upload-time = "2024-08-17T08:15:05.25Z" },
|
33 |
+
{ url = "https://files.pythonhosted.org/packages/06/97/d09338697ea0eb7386a3df0c6ca2a77ab090c19420a85acdc6f36971c6b8/jq-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c24a5f9e3807e277e19f305c8bcd0665b8b89251b053903f611969657680722", size = 421253, upload-time = "2024-08-17T08:15:07.633Z" },
|
34 |
+
{ url = "https://files.pythonhosted.org/packages/b8/c3/d020c19eca167b5085e74d2277bc3d9e35d1b4ee5bcb9076f1e26882514d/jq-1.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb484525dd801583ebd695d02f9165445a4d1b2fb560b187e6fc654911f0600e", size = 725885, upload-time = "2024-08-17T08:15:10.647Z" },
|
35 |
+
{ url = "https://files.pythonhosted.org/packages/78/b8/8f6b886856f52f3277663d2d7a199663c6ede589dd0714aac9491b82ba6e/jq-1.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddd9abdf0c1b30be1bf853d8c52187c96a51b2cbc05f40c43a37bf6a9b956807", size = 746334, upload-time = "2024-08-17T08:15:13.183Z" },
|
36 |
+
{ url = "https://files.pythonhosted.org/packages/76/c2/2fa34e480068863ab372ec91c59b10214e9f8f3ae8b6e2de61456e93bae1/jq-1.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c7464d9b88c74a7119b53f4bbf88028d07a9de9a1a279e45209b763b89d6582", size = 733716, upload-time = "2024-08-17T08:15:15.836Z" },
|
37 |
+
{ url = "https://files.pythonhosted.org/packages/2e/db/59cb84ec59247af7f7bedd2b5c88b3a4ca17253fd2cc0d40f08573f7ff72/jq-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b99761e8ec2cedb9906df4ceae33f467a377621019ef40a9a275689ac3577456", size = 720978, upload-time = "2024-08-17T08:15:17.759Z" },
|
38 |
+
{ url = "https://files.pythonhosted.org/packages/e0/6f/d04bdcc037ced716e2522ebf7a677541b8654d7855cd1404d894f1ecd144/jq-1.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1be1638f9d5f38c83440fb9626d8f78905ed5d70e926e3a664d3de1198e1ef79", size = 746431, upload-time = "2024-08-17T08:15:19.948Z" },
|
39 |
+
{ url = "https://files.pythonhosted.org/packages/84/52/f100fb2ccd467c17a2ecc186334aa7b512e49ca1a678ecc53dd4defd6e22/jq-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d7e82d58bf3afe373afb3a01f866e473bbd34f38377a2f216c6222ec028eeea", size = 750404, upload-time = "2024-08-17T08:15:22.198Z" },
|
40 |
+
{ url = "https://files.pythonhosted.org/packages/86/b4/e2459542207238d86727cf81af321ee4920497757092facf347726d64965/jq-1.8.0-cp313-cp313-win32.whl", hash = "sha256:96cb0bb35d55b19b910b12aba3d72e333ad6348a703494c7738cc4664e4410f0", size = 405691, upload-time = "2024-08-17T08:15:25.346Z" },
|
41 |
+
{ url = "https://files.pythonhosted.org/packages/ce/4d/6e1230f96052d578439eee4ea28069728f3ad4027de127a93b8c6da142f0/jq-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:53e60a87657efc365a5d9ccfea2b536cddc1ffab190e823f8645ad933b272d51", size = 417930, upload-time = "2024-08-17T08:15:28.487Z" },
|
42 |
+
]
|