Problem Statement:
Ukubuyekezwa kweproblem:
Nangona i-Copilot ifumaneka kwizicelo zayo, ukufumana iinkcukacha ezisetyenzisiweyo ezisetyenziswa kwifayile, i-SharePoint kunye nezinye izilwanyana ezinokufumaneka kubonakala lula kakhulu. Ndingathanda kakhulu kule umgangatho we-Gen AI. Enye ngosuku, ndingathanda ukubuyekeza zonke iimpawu (i-team deliverables for a Quarter in Agile Framework) kunye neempawu zayo ezisebenza kwi-team. Ngokuqhathelekileyo, i-Copilot ibonakele ukufumana iinkcukacha evela kwi-Confluence page, nto leyo elungileyo kunye nokufumaneka. Iinkqubo ezininzi, iiprojekthi kunye ne-updates zokusetyenziswa kwi-Confluence pages. Ukufum
Solution: AI Agentic Assistant Powered by Streamlit + Semantic Kernel
Isisombululo: I-AI Agentic Assistant isetyenziswe yi-Streamlit + Semantic Kernel
Ukusungulwa kwe-Agentic AI yaba yaye ndingathanda, kwaye ndingathanda ukhethe le framework njengoko isisombululo. Nangona kunjalo, kukho iingxaki: le framework kufuneka isetyenziswe, kwaye kukho isisombululo olufanelekileyo? Yintoni, kunye nezifundo ezininzi, ndingathanda ukuba uqhagamshelane ne-open-source kwaye usebenzisa i-tech stack elandelayo ukuvelisa i-AI assistant elula usebenzisa:
- Streamlit kwi-front end,
- I-Kernel yeSemantic yeManagement kunye neChain,
- I-Azure OpenAI ye-Natural Language Processing,
- I-Playwright ye-secure kunye ne-dynamic web scraping yeephepha ze-Confluence.
Yintoni it
Le mveliso uya kukunceda umlawuli we-Program kunye nabasebenzi:
-
Select a team or program name from a dropdown,
-
Automatically fetch the associated Confluence page URL,
-
Scrape key content sections from that page (like Features, Epics, Dependencies, Risks, Team Members),
-
Ask questions like “What are the team Q4 deliverables?” or “Summarize the features based on status,” etc.,
-
Display answers as summarized text.
How it works - Pseudo Code
Indlela yokusebenza - Pseudo Code
Step 1. Confluence Page LookUp.
Kwimeko yokufaka i-URL ngamanzi, yonke igama lomsebenzi lithathwe kwi-URL yayo ye-Confluence usebenzisa i-dictionary. Xa umdlali ukhethe i-"Team A" kwi-panel elungileyo, i-backend yenza ngokuzenzakalelayo i-URL eyongezwayo kwaye yenza ukuchithwa.
team_to_url_map = { "I-Team A": "https://confluence.company.com/display/TEAM_A",
"I-Team B": "https://confluence.company.com/display/TEAM_B", ... }
team_to_url_map = { "Team A": "https://confluence.company.com/display/TEAM_A",
"Team B": "https://confluence.company.com/display/TEAM_B", ... }
Step 2. Web Scraping via Playwright
Oku kuthatha ixesha elide. Okugqibela, ndiza kusetyenziswa kwe-Playwright kwi-headless browser-based scraping, nto leyo inokunceda ukulayisha iinkcukacha ze-dynamic kunye nokulawula ukuhlaziywa:
Umgangatho wokugqibela:
- [ ]Ukusebenzisa i-Python request library, ufumane i-Confluence Data usebenzisa i-API. I-mechanism ye-authentication ayikho. Ngaphandle kwalokho, kunokuba yindlela elungileyo yokufumana i-Confluence page data.
- [ ]Ukusebenzisa i-Python BeautifySoup library. Iye ilawulwa ngenxa yeengxaki ze-dynamic.
- [ ]I ended up with Python Playwright. The SSO layer had challenges, but finally, it worked after downloading the HTML state JSON and reusing it.
@kernel_function(description="Scrape and return text content from a Confluence page.") async def get_confluence_page_content(self, team_name: Annotated[str, "Name of the Agile team"]) -> Annotated[str, "Returns extracted text content from the page"):
@kernel_function(description="Scrape and return text content from a Confluence page.") async def get_confluence_page_content(self, team_name: Annotated[str, "Name of the Agile team"]) -> Annotated[str, "Returns extracted text content from the page"]:
Step 3. Define Client, Agent, and Prompt Engineering with Semantic Kernel.
Isixhobo yenzelwe ukuba yi-Program Management Enabler. I-Agent Instructions yenzelwe ukusetyenziswa kweengxaki ezincinane kunye nokwenza imiphumo efanelekileyo kwiingxaki ze-PM. Iingxaki ziya kukunceda ukufumana i-output njenge-text summary okanye kwi-chart format. Oku kwakhona isibonelo ye-low code.
Ukongezelela, ndicinga i-client njenge-AI Agent njenge-plugin.
AGENT_INSTRUCTIONS = “”
“”
client = OpenAI(<Local open source LLM>)
chat_completion_service = OpenAIChatCompletion(ai_model_id="<>",
async_client = umxhasi )
i-agent = ChatCompletionAgent( service=chat_completion_service, i-plugins=[ ConfluencePlugin() ], igama="ConfluenceAgent", imiyalezo=AGENT_INSTRUCTIONS )
AGENT_INSTRUCTIONS = “““
“““
client = OpenAI(<Local open source LLM>)
chat_completion_service = OpenAIChatCompletion(ai_model_id="<>",
async_client=client )
agent = ChatCompletionAgent( service=chat_completion_service, plugins=[ ConfluencePlugin() ], name="ConfluenceAgent", instructions=AGENT_INSTRUCTIONS )
Step 4. Decide on User Input to process the question with or without the tool.
Ndingathanda ukongeza i-LLM client eyongezelela ukuba ingxelo lwabasebenzisi i-Relevant to Program Management okanye akukho.
ekugqibeleni = await client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": "Uyazi lwekhwalithi yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki yengxaki y
completion = await client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": "You are a Judge of the content of user input. Anlyze the user's input. If it asking to scrap internal COnfluence Page for a team then it is related to Program Management. If it is not related to Program Management, provide the reply but add 'False|' to the response. If it is related to Program Management, add 'True|' to the response."}, {"role": "user", "content": user_input} ], temperature=0.5 )
Step 5. The final step is to produce the result. Here is the entire code.
Ndiyathanda ikhowudi epheleleyo. Ndiyathanda iinkcukacha zayo zokusetyenziswa kwiprojekthi. Thina kuqala ukugcina i-state.json ukuze isetyenziswe kwi-code
ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso
TEAM_URL_MAPPING = { "Team 1": "Confluence URL for Team 1", "Team 2": "Confluence URL for Team 2", "Team 3": "Confluence URL for Team 3", "Team 4": "Confluence URL for Team 4", "Team 5": "Confluence URL for Team 5", "Team 6": "Confluence URL for Team 6" }
# ---- Plugin umzekelo ----
#Bar chart kunye ubungakanani efanelekileyo def plot_bar_chart(df): status_counts = df["status"].value_counts() fig, ax = plt.subplots(figsize=(1.5, 1)) # ububanzi, ubude kwi intshi ax.bar(status_counts.index, status_counts.values, color="#4CAF50") ax.set_title("Features by Status") ax.set_ylabel("Count") # Change tick color ax.tick_params(axis='x', colors='blue', labelrotation=90) # x-ticks kwi blue, rotated axtick_params(axis='y', colors='green') # y-ticks kwi green st.pyplot(fig)
def extract_json_from_response(text): # Usebenzisa i-regex ukufumana i-array yokuqala ye-JSON kwi-text match = re.search(r"(\[\s*{.*\s*\])", text, re.DOTALL) ukuba uyifana: return match.group(1) return None
ubungakanani ConfluencePlugin: def init(self): self.default_confluence_url = "<>" load_dotenv()
@kernel_function(description="Scrape and return text content from a Confluence page.") async def get_confluence_page_content( self, team_name: Annotated\[str, "Name of the Agile team"\] ) -> Annotated\[str, "Returns extracted text content from the page"\]: print(f"Attempting to scrape Confluence page for team: '{team_name}'") # Added for debugging target_url = TEAM_URL_MAPPING.get(team_name) if not target_url: print(f"Failed to find URL for team: '{team_name}' in TEAM_URL_MAPPING.") # Added for debugging return f"❌ No Confluence URL mapped for team '{team_name}'" async with async_playwright() as p: browser = await p.chromium.launch() context = await browser.new_context(storage_state="state.json") page = await context.new_page() pages_to_scrape = \[target_url\] # Loop through each page URL and scrape the content for page_url in pages_to_scrape: await page.goto(page_url) await asyncio.sleep(30) # Wait for the page to load await page.wait_for_selector('div.refresh-module-id, table.some-jira-table') html = await page.content() soup = BeautifulSoup(html, "html.parser") body_div = soup.find("div", class_="wiki-content") or soup.body if not body_div: return "❌ Could not find content on the Confluence page." # Process the scraped content (example: extract headings) headings = soup.find_all('h2') text = body_div.get_text(separator="\\n", strip=True) return text\[:4000\] # Truncate if needed to stay within token limits await browser.close() @kernel_function(description="Summarize and structure scraped Confluence content into JSON.") async def summarize_confluence_data( self, raw_text: Annotated\[str, "Raw text scraped from the Confluence page"\], output_style: Annotated\[str, "Output style, either 'bullet' or 'json'"\] = "json" # Default to 'json' ) -> Annotated\[str, "Returns structured summary in JSON format"\]: prompt = f""" You are a Program Management Data Extractor. Your job is to analyze the following Confluence content and produce structured machine-readable output. Confluence Content: {raw_text} Instructions: - If output_style is 'bullet', return bullet points summary. - If output_style is 'json', return only valid JSON array by removing un printable characters and spaces from beginning and end. - DO NOT write explanations. - DO NOT suggest code snippets. - DO NOT wrap JSON inside triple backticks \`\`\`json - Output ONLY the pure JSON array or bullet points list. Output_style: {output_style} """ # Call OpenAI again completion = await client.chat.completions.create( model="gpt-4o", messages=\[ {"role": "system", "content": "You are a helpful Program Management Data Extractor."}, {"role": "user", "content": prompt} \], temperature=0.1 ) structured_json = completion.choices\[0\].message.content.strip() return structured_json
# ---- Load API credentials ---- load_dotenv() client = AsyncAzureOpenAI( azure_endpoint="<>", api_key=os.getenv("AZURE_API_KEY"), api_version='<>' ) chat_completion_service = OpenAIChatCompletion( ai_model_id="<>", async_client=client )
I-AGENT_INSTRUCTIONS = ""I-You is a helpful Program Management AI Agent that can help extract key information such as Team Member, Features, Epics from a confluence page.
Important: Xa abasebenzisi zibonise i-Team page, ukuthatha kuphela i-Features kunye ne-Epic ye-Team.
Xa umxholo uqala, nceda uqhagamshelane na le nqakraza: "Hello! Ndingathanda u-PM yakho. Ndiza kukunceda ukufumana i-Status ye-Features kunye ne-Epics.
Iingxaki ZOKUSEBENZA: 1. Zonke iingxaki zokuqala 'get_confluence_page_content' ukuze uqhagamshelane le page Confluence.
- Ukuba umyalezo wabasebenzisi uqala nge "Team: {team_name}.", usebenzisa le {team_name} kwi-argument 'team_name'. Umzekelo, ukuba umyalezo uye "Team: Raptor. Yintoni iimpawu ezidlulileyo?", i 'team_name' uye "Raptor". 2. Ukuba usebenzisa umyalezo, uthathe umyalezo we-bullet points. 3. Ukuba usebenzisa umyalezo we-JSON array okanye chart okanye plot. Emva koko uqhagamshelane 'summarize_confluence_data' ukusetyenziswa kweengxaki. 4. Ngokusekelwe umyalezo we-output efunyenwe ngu-username, uthathe umyalezo we-JSON okanye i-bullet points. 5. Ukuba umyalezo wabasetyenziswa umyalezo
Ukucaciswa: - Ukuba output_style yi 'bullet', faka iiphakheji yeephakheji. - Ukuba output_style yi 'json', faka kuphela i-JSON array efanelekileyo ngokucacisa iimpawu kunye neengxaki ze-un yokucacisa kwi-beginning kunye ne-end. - ISIKHUTHA i-explanations. - ISIKHUTHA iiphakheji ze-code. - ISIKHUTHA i-JSON ngaphakathi kweephakheji zeephakheji zeephakheji ```json - I-Output ONLY i-pure JSON array okanye iiphakheji yeephakheji.
Yintoni iqela uya kubandakanyeka ukuncedisa ngoku?"
Uyakwazi ukuhlaziywa kweempawu ze-username. Ukuba zibonise iqela elifanelekileyo, nqakraza idatha yakho kwiqela elifanelekileyo kunokuba ibonise izixazululo. """ agent = ChatCompletionAgent( service=chat_completion_service, plugins=[ ConfluencePlugin() ], name="ConfluenceAgent", instructions=AGENT_INSTRUCTIONS )
# ---- Main async logic ---- async def stream_response(user_input, thread=None): html_blocks = [] full_response = [] function_calls = [] parsed_json_result = None completion = await client.chat.completions.create( model="gpt-4o", messages=["role": "system", "content": "Uyazi lwekhwalithi lwekhwalithi wekhwalithi wekhwalithi. Anlyze umsebenzi wekhwalithi. If it asking to scrap internal COnfluence Page for a team then it is related to Program Management. If it is not related to Program Management, provide the answer but add 'False T' to the answer. If it is related to Program Management, add 'True T' to the
async for response in agent.invoke_stream(messages=user_input, thread=thread): print("Response:", response) thread = response.thread agent_name = response.name for item in list(response.items): if isinstance(item, FunctionCallContent): pass # You can ignore this now elif isinstance(item, FunctionResultContent): if item.name == "summarize_confluence_data": raw_content = item.result extracted_json = extract_json_from_response(raw_content) if extracted_json: try: parsed_json = json.loads(extracted_json) yield parsed_json, thread, function_calls except Exception as e: st.error(f"Failed to parse extracted JSON: {e}") else: full_response.append(raw_content) else: full_response.append(item.result) elif isinstance(item, StreamingTextContent) and item.text: full_response.append(item.text) #print("Full Response:", full_response) # After loop ends, yield final result if parsed_json_result: yield parsed_json_result, thread, function_calls else: yield ''.join(full_response), thread, function_calls
# ---- Streamlit UI Setup ---- st.set_page_config(layout="wide") left_col, right_col = st.columns([1, 1]) st.markdown("" <style> html, umzimba, [class*="css"] { font-size: 12px !important; } </style> """, unsafe_allow_html=True) # ---- Main Streamlit app ---- nge left_col: st.title(" Program Management Enabler AI") st.write("Ukuza nami malunga izinto ezahlukeneyo Wiley Program!") st.write("Ndiya kukunceda ukufumana imeko Functions kunye Epics.")
if "history" not in st.session_state: st.session_state.history = \[\] if "thread" not in st.session_state: st.session_state.thread = None if "charts" not in st.session_state: st.session_state.charts = \[\] # Each entry: {"df": ..., "title": ..., "question": ...} if "chart_dataframes" not in st.session_state: st.session_state.chart_dataframes = \[\] if st.button("🧹 Clear Chat"): st.session_state.history = \[\] st.session_state.thread = None st.rerun() # Input box at the top user_input = st.chat_input("Ask me about your team's features...") # Example: team_selected = st.session_state.get("selected_team") if st.session_state.get("selected_team") and user_input: user_input = f"Team: {st.session_state.get('selected_team')}. {user_input}" # Preserve chat history when program or team is selected if user_input and not st.session_state.get("selected_team_changed", False): st.session_state.selected_team_changed = False if user_input: df = pd.DataFrame() full_response_holder = {"text": "","df": None} with st.chat_message("assistant"): response_container = st.empty() assistant_text = "" try: chat_index = len(st.session_state.history) response_gen = stream_response(user_input, st.session_state.thread) print("Response generator started",response_gen) async def process_stream(): async for update in response_gen: nonlocal_thread = st.session_state.thread if len(update) == 3: content, nonlocal_thread, function_calls = update full_response_holder\["text"\] = content if isinstance(content, list): data = json.loads(re.sub(r'\[\\x00-\\x1F\\x7F\]', '', content.replace("\`\`\`json", "").replace("\`\`\`",""))) df = pd.DataFrame(data) df.columns = df.columns.str.lower() print("\\n📊 Features Status Chart") st.subheader("📊 Features Status Chart") plot_bar_chart(df) st.subheader("📋 Detailed Features Table") st.dataframe(df) chart_df.columns = chart_df.columns.str.lower() full_response_holder\["df"\] = chart_df elif (re.sub(r'\[\\x00-\\x1F\\x7F\]', '', content.replace("\`\`\`json", "").replace("\`\`\`","").replace(" ",""))\[0\] =="\[" and re.sub(r'\[\\x00-\\x1F\\x7F\]', '', content.replace("\`\`\`json", "").replace("\`\`\`","").replace(" ",""))\[-1\] == "\]"): data = json.loads(re.sub(r'\[\\x00-\\x1F\\x7F\]', '', content.replace("\`\`\`json", "").replace("\`\`\`",""))) df = pd.DataFrame(data) df.columns = df.columns.str.lower() chart_df = pd.DataFrame(data) chart_df.columns = chart_df.columns.str.lower() full_response_holder\["df"\] = chart_df else: if function_calls: st.markdown("\\n".join(function_calls)) flagtext = 'text' st.session_state.thread = nonlocal_thread try: with st.spinner("🤖 AI is thinking..."): flagtext = None # Run the async function to process the stream asyncio.run(process_stream()) # Update history with the assistant's response if full_response_holder\["df"\] is not None and flagtext is None: st.session_state.chart_dataframes.append({ "question": user_input, "data": full_response_holder\["df"\], "type": "chart" }) elif full_response_holder\["text"\].strip(): # Text-type response st.session_state.history.append({ "user": user_input, "assistant": full_response_holder\["text"\], "type": "text" }) flagtext = None except Exception as e: error_msg = f"⚠️ Error: {e}" response_container.markdown(error_msg) if chat_index > 0 and "Error" in full_response_holder\["text"\]: # Remove the last message only if it was an error st.session_state.history.pop(chat_index) # Handle any exceptions that occur during the async call except Exception as e: full_response_holder\["text"\] = f"⚠️ Error: {e}" response_container.markdown(full_response_holder\["text"\]) chat_index = len(st.session_state.history) #for item in st.session_state.history\[:-1\]: for item in reversed(st.session_state.history): if item\["type"\] == "text": with st.chat_message("user"): st.markdown(item\["user"\]) with st.chat_message("assistant"): st.markdown(item\["assistant"\])
with right_col:st.title("Ukuhlola i-Wiley Program")
team_list = { "Program 1": \["Team 1", "Team 2", "Team 3"\], "Program 2": \["Team 4", "Team 5", "Team 6"\] } selected_program = st.selectbox("Select the Program:", \["No selection"\] + list(team_list.keys()), key="program_selectbox") selected_team = st.selectbox("Select the Agile Team:", \["No selection"\] + team_list.get(selected_program, \[\]), key="team_selectbox") st.session_state\["selected_team"\] = selected_team if selected_team != "No selection" else None if st.button("🧹 Clear All Charts"): st.session_state.chart_dataframes = \[\] chart_idx = 1 #if len(st.session_state.chart_dataframes) == 1: for idx, item in enumerate(st.session_state.chart_dataframes): #for idx, item in enumerate(st.session_state.chart_dataframes): st.markdown(f"\*\*Chart {idx + 1}: {item\['question'\]}\*\*") st.subheader("📊 Features Status Chart") plot_bar_chart(item\["data"\]) st.subheader("📋 Detailed Features Table") st.dataframe(item\["data"\]) chart_idx += 1
ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso ubhaliso
TEAM_URL_MAPPING = { "Team 1": "Confluence URL for Team 1", "Team 2": "Confluence URL for Team 2", "Team 3": "Confluence URL for Team 3", "Team 4": "Confluence URL for Team 4", "Team 5": "Confluence URL for Team 5", "Team 6": "Confluence URL for Team 6" }
# ---- Plugin umzekelo ----
#Bar chart with fixed size def plot_bar_chart(df): status_counts = df["status"].value_counts() fig, ax = plt.subplots(figsize=(1.5, 1)) # width, height in inches ax.bar(status_counts.index, status_counts.values, color="#4CAF50") ax.set_title("Features by Status") ax.set_ylabel("Count") # Change tick color ax.tick_params(axis='x', colors='blue', labelrotation=90) # x-ticks in blue, rotated ax.tick_params(axis='y', colors='green') # y-ticks in green st.pyplot(fig)
def extract_json_from_response(text): # Usebenzisa i-regex ukufumana i-array yokuqala ye-JSON kwi-text match = re.search(r"(\[\s*{.*\s*\])", text, re.DOTALL) ukuba uyifana: return match.group(1) return None
isixeko Confluence: def
@kernel_function(description="Scrape and return text content from a Confluence page.")
async def get_confluence_page_content(
self, team_name: Annotated\[str, "Name of the Agile team"\]
) -> Annotated\[str, "Returns extracted text content from the page"\]:
print(f"Attempting to scrape Confluence page for team: '{team_name}'") # Added for debugging
target_url = TEAM_URL_MAPPING.get(team_name)
if not target_url:
print(f"Failed to find URL for team: '{team_name}' in TEAM_URL_MAPPING.") # Added for debugging
return f"❌ No Confluence URL mapped for team '{team_name}'"
async with async_playwright() as p:
browser = await p.chromium.launch()
context = await browser.new_context(storage_state="state.json")
page = await context.new_page()
pages_to_scrape = \[target_url\]
# Loop through each page URL and scrape the content
for page_url in pages_to_scrape:
await page.goto(page_url)
await asyncio.sleep(30) # Wait for the page to load
await page.wait_for_selector('div.refresh-module-id, table.some-jira-table')
html = await page.content()
soup = BeautifulSoup(html, "html.parser")
body_div = soup.find("div", class_="wiki-content") or soup.body
if not body_div:
return "❌ Could not find content on the Confluence page."
# Process the scraped content (example: extract headings)
headings = soup.find_all('h2')
text = body_div.get_text(separator="\\n", strip=True)
return text\[:4000\] # Truncate if needed to stay within token limits
await browser.close()
@kernel_function(description="Summarize and structure scraped Confluence content into JSON.")
async def summarize_confluence_data(
self, raw_text: Annotated\[str, "Raw text scraped from the Confluence page"\],
output_style: Annotated\[str, "Output style, either 'bullet' or 'json'"\] = "json" # Default to 'json'
) -> Annotated\[str, "Returns structured summary in JSON format"\]:
prompt = f"""
You are a Program Management Data Extractor.
Your job is to analyze the following Confluence content and produce structured machine-readable output.
Confluence Content:
{raw_text}
Instructions:
- If output_style is 'bullet', return bullet points summary.
- If output_style is 'json', return only valid JSON array by removing un printable characters and spaces from beginning and end.
- DO NOT write explanations.
- DO NOT suggest code snippets.
- DO NOT wrap JSON inside triple backticks \`\`\`json
- Output ONLY the pure JSON array or bullet points list.
Output_style: {output_style}
"""
# Call OpenAI again
completion = await client.chat.completions.create(
model="gpt-4o",
messages=\[
{"role": "system", "content": "You are a helpful Program Management Data Extractor."},
{"role": "user", "content": prompt}
\],
temperature=0.1
)
structured_json = completion.choices\[0\].message.content.strip()
return structured_json
# ---- Load API credentials ---- load_dotenv() client = AsyncAzureOpenAI( azure_endpoint="<>", api_key=os.getenv("AZURE_API_KEY"), api_version='<>' ) chat_completion_service = OpenAIChatCompletion( ai_model_id="<>", async_client=client )
I-AGENT_INSTRUCTIONS = ""I-You is a helpful Program Management AI Agent that can help extract key information such as Team Member, Features, Epics from a confluence page.
Important: Xa abasebenzisi zibonise i-Team page, ukuthatha kuphela i-Features kunye ne-Epic ye-Team.
Xa umxholo uqala, nceda uqhagamshelane na le nqakraza: "Hello! Ndingathanda u-PM yakho. Ndiza kukunceda ukufumana i-Status ye-Features kunye ne-Epics.
Iingxaki ZOKUSEBENZA: 1. Zonke iingxaki zokuqala 'get_confluence_page_content' ukuze uqhagamshelane le page Confluence.
- Ukuba umyalezo wabasebenzisi uqala nge "Team: {team_name}.", usebenzisa le {team_name} kwi-argument 'team_name'. Umzekelo, ukuba umyalezo uye "Team: Raptor. Yintoni iimpawu ezidlulileyo?", i 'team_name' uye "Raptor". 2. Ukuba usebenzisa umyalezo, uthathe umyalezo we-bullet points. 3. Ukuba usebenzisa umyalezo we-JSON array okanye chart okanye plot. Emva koko uqhagamshelane 'summarize_confluence_data' ukusetyenziswa kweengxaki. 4. Ngokusekelwe umyalezo we-output efunyenwe ngu-username, uthathe umyalezo we-JSON okanye i-bullet points. 5. Ukuba umyalezo wabasetyenziswa umyalezo
Ukucaciswa: - Ukuba output_style yi 'bullet', faka iiphakheji yeephakheji. - Ukuba output_style yi 'json', faka kuphela i-JSON array efanelekileyo ngokucacisa iimpawu kunye neengxaki ze-un yokucacisa kwi-beginning kunye ne-end. - ISIKHUTHA i-explanations. - ISIKHUTHA iiphakheji ze-code. - ISIKHUTHA i-JSON ngaphakathi kweephakheji zeephakheji zeephakheji ```json - I-Output ONLY i-pure JSON array okanye iiphakheji yeephakheji.
Yintoni iqela uya kubandakanyeka ukuncedisa ngoku?"
Uyakwazi ukuhlaziywa kweempawu ze-username. Ukuba zibonise iqela elifanelekileyo, nqakraza idatha yakho kwiqela elifanelekileyo kunokuba ibonise izixazululo. """ agent = ChatCompletionAgent( service=chat_completion_service, plugins=[ ConfluencePlugin() ], name="ConfluenceAgent", instructions=AGENT_INSTRUCTIONS )
# ---- Main async logic ---- async def stream_response(user_input, thread=None): html_blocks = [] full_response = [] function_calls = [] parsed_json_result = None completion = await client.chat.completions.create( model="gpt-4o", messages=["role": "system", "content": "Uyazi lwekhwalithi lwekhwalithi wekhwalithi wekhwalithi. Anlyze umsebenzi wekhwalithi. If it asking to scrap internal COnfluence Page for a team then it is related to Program Management. If it is not related to Program Management, provide the answer but add 'False T' to the answer. If it is related to Program Management, add 'True T' to the
async for response in agent.invoke_stream(messages=user_input, thread=thread):
print("Response:", response)
thread = response.thread
agent_name = response.name
for item in list(response.items):
if isinstance(item, FunctionCallContent):
pass # You can ignore this now
elif isinstance(item, FunctionResultContent):
if item.name == "summarize_confluence_data":
raw_content = item.result
extracted_json = extract_json_from_response(raw_content)
if extracted_json:
try:
parsed_json = json.loads(extracted_json)
yield parsed_json, thread, function_calls
except Exception as e:
st.error(f"Failed to parse extracted JSON: {e}")
else:
full_response.append(raw_content)
else:
full_response.append(item.result)
elif isinstance(item, StreamingTextContent) and item.text:
full_response.append(item.text)
#print("Full Response:", full_response)
# After loop ends, yield final result
if parsed_json_result:
yield parsed_json_result, thread, function_calls
else:
yield ''.join(full_response), thread, function_calls
# ---- Streamlit UI Setup ---- st.set_page_config(layout="wide") left_col, right_col = st.columns([1, 1]) st.markdown("" <style> html, umzimba, [class*="css"] { font-size: 12px !important; } </style> """, unsafe_allow_html=True) # ---- Main Streamlit app ---- nge left_col: st.title(" Program Management Enabler AI") st.write("Ukuza nami malunga izinto ezahlukeneyo Wiley Program!") st.write("Ndiya kukunceda ukufumana imeko Functions kunye Epics.")
if "history" not in st.session_state:
st.session_state.history = \[\]
if "thread" not in st.session_state:
st.session_state.thread = None
if "charts" not in st.session_state:
st.session_state.charts = \[\] # Each entry: {"df": ..., "title": ..., "question": ...}
if "chart_dataframes" not in st.session_state:
st.session_state.chart_dataframes = \[\]
if st.button("🧹 Clear Chat"):
st.session_state.history = \[\]
st.session_state.thread = None
st.rerun()
# Input box at the top
user_input = st.chat_input("Ask me about your team's features...")
# Example:
team_selected = st.session_state.get("selected_team")
if st.session_state.get("selected_team") and user_input:
user_input = f"Team: {st.session_state.get('selected_team')}. {user_input}"
# Preserve chat history when program or team is selected
if user_input and not st.session_state.get("selected_team_changed", False):
st.session_state.selected_team_changed = False
if user_input:
df = pd.DataFrame()
full_response_holder = {"text": "","df": None}
with st.chat_message("assistant"):
response_container = st.empty()
assistant_text = ""
try:
chat_index = len(st.session_state.history)
response_gen = stream_response(user_input, st.session_state.thread)
print("Response generator started",response_gen)
async def process_stream():
async for update in response_gen:
nonlocal_thread = st.session_state.thread
if len(update) == 3:
content, nonlocal_thread, function_calls = update
full_response_holder\["text"\] = content
if isinstance(content, list):
data = json.loads(re.sub(r'\[\\x00-\\x1F\\x7F\]', '', content.replace("\`\`\`json", "").replace("\`\`\`","")))
df = pd.DataFrame(data)
df.columns = df.columns.str.lower()
print("\\n📊 Features Status Chart")
st.subheader("📊 Features Status Chart")
plot_bar_chart(df)
st.subheader("📋 Detailed Features Table")
st.dataframe(df)
chart_df.columns = chart_df.columns.str.lower()
full_response_holder\["df"\] = chart_df
elif (re.sub(r'\[\\x00-\\x1F\\x7F\]', '', content.replace("\`\`\`json", "").replace("\`\`\`","").replace(" ",""))\[0\] =="\[" and re.sub(r'\[\\x00-\\x1F\\x7F\]', '', content.replace("\`\`\`json", "").replace("\`\`\`","").replace(" ",""))\[-1\] == "\]"):
data = json.loads(re.sub(r'\[\\x00-\\x1F\\x7F\]', '', content.replace("\`\`\`json", "").replace("\`\`\`","")))
df = pd.DataFrame(data)
df.columns = df.columns.str.lower()
chart_df = pd.DataFrame(data)
chart_df.columns = chart_df.columns.str.lower()
full_response_holder\["df"\] = chart_df
else:
if function_calls:
st.markdown("\\n".join(function_calls))
flagtext = 'text'
st.session_state.thread = nonlocal_thread
try:
with st.spinner("🤖 AI is thinking..."):
flagtext = None
# Run the async function to process the stream
asyncio.run(process_stream())
# Update history with the assistant's response
if full_response_holder\["df"\] is not None and flagtext is None:
st.session_state.chart_dataframes.append({
"question": user_input,
"data": full_response_holder\["df"\],
"type": "chart"
})
elif full_response_holder\["text"\].strip():
# Text-type response
st.session_state.history.append({
"user": user_input,
"assistant": full_response_holder\["text"\],
"type": "text"
})
flagtext = None
except Exception as e:
error_msg = f"⚠️ Error: {e}"
response_container.markdown(error_msg)
if chat_index > 0 and "Error" in full_response_holder\["text"\]:
# Remove the last message only if it was an error
st.session_state.history.pop(chat_index)
# Handle any exceptions that occur during the async call
except Exception as e:
full_response_holder\["text"\] = f"⚠️ Error: {e}"
response_container.markdown(full_response_holder\["text"\])
chat_index = len(st.session_state.history)
#for item in st.session_state.history\[:-1\]:
for item in reversed(st.session_state.history):
if item\["type"\] == "text":
with st.chat_message("user"):
st.markdown(item\["user"\])
with st.chat_message("assistant"):
st.markdown(item\["assistant"\])
with right_col:st.title("Ukuhlola i-Wiley Program")
team_list = {
"Program 1": \["Team 1", "Team 2", "Team 3"\],
"Program 2": \["Team 4", "Team 5", "Team 6"\]
}
selected_program = st.selectbox("Select the Program:", \["No selection"\] + list(team_list.keys()), key="program_selectbox")
selected_team = st.selectbox("Select the Agile Team:", \["No selection"\] + team_list.get(selected_program, \[\]), key="team_selectbox")
st.session_state\["selected_team"\] = selected_team if selected_team != "No selection" else None
if st.button("🧹 Clear All Charts"):
st.session_state.chart_dataframes = \[\]
chart_idx = 1
#if len(st.session_state.chart_dataframes) == 1:
for idx, item in enumerate(st.session_state.chart_dataframes):
#for idx, item in enumerate(st.session_state.chart_dataframes):
st.markdown(f"\*\*Chart {idx + 1}: {item\['question'\]}\*\*")
st.subheader("📊 Features Status Chart")
plot_bar_chart(item\["data"\])
st.subheader("📋 Detailed Features Table")
st.dataframe(item\["data"\])
chart_idx += 1
Ukucinga
YintoniStreamlit-based Program Management AI chatbotinikeza iindidi ukuyifaka iimpawu zeeprojekthi kunye neepics kwiiphepha zeConfluence. Le app isebenzisaSemantic Kernel agentskunye ne-OpenAI GPT-4o ukusika i-Team-specific Confluence page content usebenzisaPlaywright. I-State isetyenziselwa ukulayishwa. I-tool ivumela ukwahlukanisa i-Program kunye neqela elifanayo, kwaye phantsi kolawulo, ingxelo lomsebenzisi iya kuxhaswa. Nge-Agentic AI feature, sinokukwazi ukwenza i-LLM yobugcisa yobugcisa yobugcisa. I-LLM inokukwazi ukunciphisa ukufikelela kwe-LLM kwiinkcukacha, kodwa nangokufumana i-LLM feature yeedatha ezincinciphekileyo. Yinto isibonelo yokufumana i-Agentic AI feature kunye ne-power it.
Ukucaciswa:https://github.com/microsoft/ai-agents-for-beginners?tab=readme-ov-file
Playwright documentation.