338 lekti
338 lekti

Ki jan AI se revolisyon nan jesyon pwogram Agile ak Confluence & Streamlit

pa Ravikant Singh23m2025/06/14
Read on Terminal Reader

Twò lontan; Pou li

Yon ajan AI retire ak rezime done pwojè soti nan Confluence, ranfòse jesyon pwogram Agile ak rapò chat-powered, grafik, ak konesans.
featured image - Ki jan AI se revolisyon nan jesyon pwogram Agile ak Confluence & Streamlit
Ravikant Singh HackerNoon profile picture
0-item
1-item
2-item

Problem Statement:

Problèm Deklarasyon:

avèk Copilot entegre nan aplikasyon yo nan òganizasyon an, jwenn done rare itilize soti nan dosye, SharePoint, ak lòt sous aksè te vin trè fasil. Mwen te depann anpil sou kapasite sa a Gen AI. Yon jou, mwen te bezwen yon rezime nan tout karakteristik yo (tèm livrezon pou yon Quarter nan Agile Framework) ak estati yo ke ekip la ap travay sou. Malgre ke, Copilot refize li done soti nan paj la Confluence, ki se ideyal ak espere. Anpil òganizasyon, pwojè, ak ajou pwogram yo estoke sou paj la Confluence. Jwenn yon rezime nan objektif ekip, livrezon, risk pwojè, ak estati kapab tan-consuming pou yon lidè oswa yon moun ki ap travay ak plizyè pwogram. Mwen panse,


Solution: AI Agentic Assistant Powered by Streamlit + Semantic Kernel

Solution: AI Agentic Assistant Powered by Streamlit + Semantic Kernel

Envantè a nan Agentic AI te yon sove pou m ', epi mwen te deside chwazi ankadreman sa a kòm yon solisyon. Sepandan, te gen defi: ki ankadreman ta dwe itilize, ak gen yon sous louvri ki disponib? Ki sa ki koute platfòm la jesyon ta dwe? Finalman, ak tout rechèch, mwen te deside ale ak sous louvri ak sèvi ak piki a teknoloji anba a yo bati yon asistan AI limyè lè l sèvi avèk:

  • Streamlit pou fen an nan devan,
  • Kernèl semantik pou jesyon rapid ak chaj,
  • Azure OpenAI pou pwosesis lang natirèl,
  • Playwright pou sekirite ak dinamik web scraping nan paj Confluence.

Ki sa ki fè

Zouti sa a pral pèmèt manadjè pwogram ak lidè yo:

  • 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

Ki jan li travay - Pseudo Kòd

Step 1. Confluence Page LookUp.

Kòm non nan manyen mete URLs, chak non ekip se mape nan URL Confluence li yo lè l sèvi avèk yon dictionaire. Lè yon itilizatè chwazi "Team A" soti nan panèl la dwat, backend la otomatikman retire URL a ki asosye ak trigger scraping.


team_to_url_map = { "Team A": "https://confluence.company.com/display/TEAM_A",

"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

Finalman, mwen te fini lè l sèvi avèk Playwright pou scraping ki baze sou navigatè ki pa tèt, ki ede nou chaje kontni dinamik ak jere login:

Manke apwòch la -

  • [ ]Se lè l sèvi avèk Bibliyotèk python demann, jwenn done a Confluence lè l sèvi avèk API a. Mekanis la autentifikasyon te pa siksè. Antan, li ta dwe yon bon fason pou jwenn done paj Confluence.
  • [ ]Se lè l sèvi avèk Bibliyotèk la BeautifySoup nan Python. Li te exclue paske nan kontni dinamik.
  • [ ] Mwen te fini ak Python Playwright la. Layer la SSO te gen pwoblèm, men finalman, li te travay apre yo telechaje estati a HTML JSON ak re itilize li.


@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, "Retourne ekstrè tèks kontni soti nan paj la"):

@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.

Zouti a se pou yo dwe yon Enabler jesyon pwogram. Enstriksyon yo nan ajan yo fèt yo sèvi ak kontni a rasin ak pwodwi yon rezilta apwopriye pou PM kesyon. Enstriksyon yo pral ede jwenn pwodiksyon an kòm yon rezime tèks oswa nan yon fòma grafik. Sa a se tou yon egzanp nan koòd ki ba.

Anplis de sa, mwen definye kliyan a kòm yon ajan AI kòm yon Plugin.

AGENT_INSTRUCTIONS = “”

»»»

Kliyan = OpenAI (<Local Open Source LLM>)

chat_completion_service = OpenAIChatCompletion(ai_model_id="<>",

async_client = kliyan nan

agent = ChatCompletionAgent( sèvis=chat_completion_service, Plugins=[ ConfluencePlugin() ], non="ConfluenceAgent", enstriksyon=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.

Mwen te deside ajoute yon kliyan LLM adisyonèl yo tcheke si entwodiksyon itilizatè a se enpòtan pou jesyon pwogram oswa pa.

completion = await client.chat.completions.create( model="gpt-4o", messages=[ {"role": "sistèm", "konten": "Ou se yon Jwenn nan kontni a nan entèlijans itilizatè a. Anlyze entèlijans a itilizatè a. Si li mande yo retire enteryè COnfluence Paj pou yon ekip, Lè sa a, li se ki gen rapò ak jesyon pwogram. Si li pa gen rapò ak jesyon pwogram, bay repons la men ajoute 'False gc' nan repons la. Si li se ki gen rapò ak jesyon pwogram, ajoute 'True gc' nan repons la."}, {"role": "user", "content": user_input} ], tanperati=0.5 )

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.

Isit la se tout kòd la. Mwen retire detay pwojè espesifik mwen. Nou bezwen sove state.json an premye yo sèvi ak li nan kòd la



import json import os import asyncio import pandas kòm pd import streamlit kòm st soti nan typing import Anoté soti nan dotenv import load_dotenv soti nan openai import AsyncAzureOpenAI soti nan playwright.async_api import async_playwright soti nan bs4 import BeautifulSoup soti nan semantic_kernel.functions import kernel_function soti nan typing import Anoté import re import matplot.pyplot soti nan openai AsyncAzureOpenAI soti nan semantic_kernel.agents import ChatCompletionAgent soti nan semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion soti nan semantic_kernel.contents import FunCallContent, FunctionRes

TEAM_URL_MAPPING = { "Team 1": "Confluence URL pou Ekip 1", "Team 2": "Confluence URL pou Ekip 2", "Team 3": "Confluence URL pou Ekip 3", "Team 4": "Confluence URL pou Ekip 4", "Team 5": "Confluence URL pou Ekip 5", "Team 6": "Confluence URL pou Ekip 6" }

# ---- Defini Plugin ----

#bar chart ak gwosè fixe def plot_bar_chart(df): status_counts = df["status"].value_counts() fig, ax = plt.subplots(figsize=(1.5, 1)) # width, height nan pous 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 nan ble, rotated axtick_params(axis='y', colors='green') # y-ticks nan green st.pyplot(fig)

def extract_json_from_response(text): # Sèvi ak regex yo jwenn matche a JSON premye nan tèks la = re.search(r"(\[\s*{.*\s*\])", tèks, re.DOTALL) si matche: return match.group(1) return Pa gen anyen

klas 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 )

AGENT_INSTRUCTIONS = """Ou se yon manadjè AI manadjè pwogram ki ka ede ekstrè enfòmasyon kle tankou manm ekip, karakteristik, Epics soti nan yon paj konfli.

Enpòtan: Lè itilizatè spécifye yon paj ekip, sèlman ekstrè karakteristik yo ak Epics nan ekip la.

Lè konvèsasyon an kòmanse, prezante tèt ou ak mesaj sa a: "Hello! Mwen se asistan PM ou. Mwen ka ede ou jwenn estati a nan karakteristik ak Epics.

Etap yo ou dwe swiv: 1. Toujou premye rele 'get_confluence_page_content' yo retire paj la Confluence.

  • If the user's message starts with "Team: {team_name}.", use that {team_name} for the `team_name` argument. For example, if the input is "Team: Raptor. What are the latest features?", the `team_name` is "Raptor". 2. If the user asks for a summary, provide a bullet points list. 3. If the user asks for a JSON array or chart or plot. Then immediately call `summarize_confluence_data` using the scraped content. 4. Based on the output style requested by the user, return either a JSON array or bullet points. 5. If the user doesn't specify an output style, default to bullet point summary. 6. If the user asks for a JSON array, return only valid JSON and plot chart/graph.

Enstriksyon yo: - Si output_style se 'bullet', retounen rezime pwen boulèt. - Si output_style se 'json', retounen sèlman matris JSON ki valab pa retire karaktè ki pa ka enprime ak espas soti nan kòmansman an ak fen. - pa ekri eksplikasyon. - pa sijere koutim kòd. - pa envile JSON nan twa boulèt ``'json - Retounen sèlman lis la nan matris JSON pur oswa pwen boulèt.

Ki ekip ou enterese nan ede ou planifye jodi a?"

Toujou priorite preferans itilizatè. Si yo rele yon ekip espesifik, konsantre done ou sou ekip sa a anvan sijere alternativ. """ ajan = ChatCompletionAgent( service=chat_completion_service, plugins=[ ConfluencePlugin() ], name="ConfluenceAgent", enstriksyon=AGENT_INSTRUCTIONS )



# ---- Main async logic ---- async def stream_response(user_input, thread=None): html_blocks = [] full_response = [] function_calls = [] parsed_json_result = Pa gen okenn konplèksyon = espere client.chat.completions.create( modèl="gpt-4o", mesaj=["role": "sistèm", "Content": "Si li se ki gen rapò ak Program Management, ajoute 'True Fight' nan repons la."}, {role function": "user", "Content_Input Management" pwosesis la kontinye, tèm tèm: tèm: 0.=5 ) repons =_text_choices__output____text_wow___related].[st

    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 Konfigirasyon ---- st.set_page_config(layout="wide") left_col, right_col = st.columns([1, 1]) st.markdown("" <style> html, kò, [class*="css"] { font-dimansyon: 12px !important; } </style> """, unsafe_allow_html=True) # ---- Main Streamlit app ---- ak left_col: st.title(" Program Management Enabler AI") st.write("Priye m 'sou diferan Wiley Program komite atik.!") st.write("Mwen ka ede w jwenn estati a nan karakteristik ak 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"\])


ak right_col:st.title("Select 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


import json import os import asyncio import pandas kòm pd import streamlit kòm st soti nan typing import Anoté soti nan dotenv import load_dotenv soti nan openai import AsyncAzureOpenAI soti nan playwright.async_api import async_playwright soti nan bs4 import BeautifulSoup soti nan semantic_kernel.functions import kernel_function soti nan typing import Anoté import re import matplot.pyplot soti nan openai AsyncAzureOpenAI soti nan semantic_kernel.agents import ChatCompletionAgent soti nan semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion soti nan semantic_kernel.contents import FunCallContent, FunctionRes

TEAM_URL_MAPPING = { "Team 1": "Confluence URL pou Ekip 1", "Team 2": "Confluence URL pou Ekip 2", "Team 3": "Confluence URL pou Ekip 3", "Team 4": "Confluence URL pou Ekip 4", "Team 5": "Confluence URL pou Ekip 5", "Team 6": "Confluence URL pou Ekip 6" }

# ---- Defini Plugin ----

#bar chart ak gwosè fixe def plot_bar_chart(df): status_counts = df["status"].value_counts() fig, ax = plt.subplots(figsize=(1.5, 1)) # width, height nan pous 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 nan ble, rotated axtick_params(axis='y', colors='green') # y-ticks nan green st.pyplot(fig)

def extract_json_from_response(text): # Sèvi ak regex yo jwenn matche a JSON premye nan tèks la = re.search(r"(\[\s*{.*\s*\])", tèks, re.DOTALL) si matche: return match.group(1) return Pa gen anyen

klas ConfluencePlugin: definit(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 )

AGENT_INSTRUCTIONS = """Ou se yon manadjè AI manadjè pwogram ki ka ede ekstrè enfòmasyon kle tankou manm ekip, karakteristik, Epics soti nan yon paj konfli.

Enpòtan: Lè itilizatè spécifye yon paj ekip, sèlman ekstrè karakteristik yo ak Epics nan ekip la.

Lè konvèsasyon an kòmanse, prezante tèt ou ak mesaj sa a: "Hello! Mwen se asistan PM ou. Mwen ka ede ou jwenn estati a nan karakteristik ak Epics.

Etap yo ou dwe swiv: 1. Toujou premye rele 'get_confluence_page_content' yo retire paj la Confluence.

  • Si mesaj la itilizatè a kòmanse ak "Team: {team_name}.", sèvi ak sa a {team_name} pou 'team_name' argomant. Pou egzanp, si envantè a se "Team: Raptor. Ki sa yo karakteristik dènye?", 'team_name' se "Raptor". 2. Si itilizatè a mande pou yon rezime, bay yon lis pwen boulèt. 3. Si itilizatè a mande pou yon array JSON oswa chart oswa plot. Lè sa a, imedyatman rele 'summarize_confluence_data' lè l sèvi avèk kontni a ranpli. 4. Dapre style la pwodiksyon mande pa itilizatè a, kite tou de yon array JSON oswa pwen boulèt. 5. Si itilizatè a pa mete yon style pwodiksyon, default nan pwen boulèt. 6. Si itiliz

Enstriksyon yo: - Si output_style se 'bullet', retounen rezime pwen boulèt. - Si output_style se 'json', retounen sèlman matris JSON ki valab pa retire karaktè ki pa ka enprime ak espas soti nan kòmansman an ak fen. - pa ekri eksplikasyon. - pa sijere koutim kòd. - pa envile JSON nan twa boulèt ``'json - Retounen sèlman lis la nan matris JSON pur oswa pwen boulèt.

Ki ekip ou enterese nan ede ou planifye jodi a?"

Toujou priorite preferans itilizatè. Si yo rele yon ekip espesifik, konsantre done ou sou ekip sa a anvan sijere alternativ. """ ajan = ChatCompletionAgent( service=chat_completion_service, plugins=[ ConfluencePlugin() ], name="ConfluenceAgent", enstriksyon=AGENT_INSTRUCTIONS )



# ---- Main async logic ---- async def stream_response(user_input, thread=None): html_blocks = [] full_response = [] function_calls = [] parsed_json_result = Pa gen okenn konplèksyon = espere client.chat.completions.create( modèl="gpt-4o", mesaj=["role": "sistèm", "Content": "Si li se ki gen rapò ak Program Management, ajoute 'True Fight' nan repons la."}, {role function": "user", "Content_Input Management" pwosesis la kontinye, tèm tèm: tèm: 0.=5 ) repons =_text_choices__output____text_wow___related].[st

    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 Konfigirasyon ---- st.set_page_config(layout="wide") left_col, right_col = st.columns([1, 1]) st.markdown("" <style> html, kò, [class*="css"] { font-dimansyon: 12px !important; } </style> """, unsafe_allow_html=True) # ---- Main Streamlit app ---- ak left_col: st.title(" Program Management Enabler AI") st.write("Priye m 'sou diferan Wiley Program komite atik.!") st.write("Mwen ka ede w jwenn estati a nan karakteristik ak 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"\])


ak right_col:st.title("Select 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


Konklisyon

nanStreamlit-based Program Management AI chatbotede ekip yo kontwole karakteristik pwojè ak epik soti nan paj Confluence. Aplikasyon an sèvi akSemantic Kernel agentsak OpenAI GPT-4o pou rasin kontni paj Confluence espesifik ekip lè l sèvi avèkPlaywright. Eta a se itilize pou otantifikasyon. Zouti a pèmèt seleksyon an nan Pwogram la ak ekip la ki gen rapò, ak ki baze sou seleksyon an, entwodiksyon nan itilizatè a pral reponn. Avèk fonksyon an Agentic AI, nou ka pèmèt LLM yo dwe yon assistant pèsonèl reyèl. Li ka potansyèl nan limite aksè a nan LLM a nan done a, men toujou sèvi ak fonksyon an LLM nan done restriksyon. Li se yon egzanp pou konprann fonksyon an Agentic AI ak ki jan potansyèl li kapab.


Referans nan:https://github.com/microsoft/ai-agents-for-beginners?tab=readme-ov-file

              Playwright documentation. 


Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks