Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +159 -38
src/streamlit_app.py
CHANGED
|
@@ -1,40 +1,161 @@
|
|
| 1 |
-
|
| 2 |
-
import
|
| 3 |
-
import pandas as pd
|
| 4 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
""
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
""
|
| 15 |
-
|
| 16 |
-
num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
|
| 17 |
-
num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
|
| 18 |
-
|
| 19 |
-
indices = np.linspace(0, 1, num_points)
|
| 20 |
-
theta = 2 * np.pi * num_turns * indices
|
| 21 |
-
radius = indices
|
| 22 |
-
|
| 23 |
-
x = radius * np.cos(theta)
|
| 24 |
-
y = radius * np.sin(theta)
|
| 25 |
-
|
| 26 |
-
df = pd.DataFrame({
|
| 27 |
-
"x": x,
|
| 28 |
-
"y": y,
|
| 29 |
-
"idx": indices,
|
| 30 |
-
"rand": np.random.randn(num_points),
|
| 31 |
-
})
|
| 32 |
-
|
| 33 |
-
st.altair_chart(alt.Chart(df, height=700, width=700)
|
| 34 |
-
.mark_point(filled=True)
|
| 35 |
-
.encode(
|
| 36 |
-
x=alt.X("x", axis=None),
|
| 37 |
-
y=alt.Y("y", axis=None),
|
| 38 |
-
color=alt.Color("idx", legend=None, scale=alt.Scale()),
|
| 39 |
-
size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
|
| 40 |
-
))
|
|
|
|
| 1 |
+
from agents import Agent, Runner
|
| 2 |
+
from composio_openai_agents import ComposioToolSet, App, Action
|
|
|
|
| 3 |
import streamlit as st
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
import asyncio
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import plotly.express as px
|
| 9 |
+
from wordcloud import WordCloud
|
| 10 |
+
import matplotlib.pyplot as plt
|
| 11 |
+
from io import BytesIO
|
| 12 |
+
import base64
|
| 13 |
+
from pydantic import BaseModel
|
| 14 |
+
from typing import List, Optional
|
| 15 |
+
import os
|
| 16 |
+
|
| 17 |
+
load_dotenv()
|
| 18 |
+
|
| 19 |
+
max_date = datetime.now().strftime("%Y-%m-%d")
|
| 20 |
+
min_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
| 21 |
+
|
| 22 |
+
class SentimentDatum(BaseModel):
|
| 23 |
+
sentiment: str
|
| 24 |
+
count: int
|
| 25 |
+
|
| 26 |
+
class IncidentDatum(BaseModel):
|
| 27 |
+
source: str
|
| 28 |
+
incidents: int
|
| 29 |
+
|
| 30 |
+
class CompetitiveDatum(BaseModel):
|
| 31 |
+
company: str
|
| 32 |
+
mentions: int
|
| 33 |
+
|
| 34 |
+
class Citation(BaseModel):
|
| 35 |
+
source: str
|
| 36 |
+
text: str
|
| 37 |
+
url: str
|
| 38 |
+
sentiment: str
|
| 39 |
+
|
| 40 |
+
class DashboardOutput(BaseModel):
|
| 41 |
+
executive_summary: str
|
| 42 |
+
sentiment_data: List[SentimentDatum]
|
| 43 |
+
incident_data: List[IncidentDatum]
|
| 44 |
+
competitive_data: List[CompetitiveDatum]
|
| 45 |
+
wordcloud_text: str
|
| 46 |
+
citations: List[Citation]
|
| 47 |
+
|
| 48 |
+
openai_key = os.environ.get("OPENAI_API_KEY")
|
| 49 |
+
composio_key = os.environ.get("COMPOSIO_API_KEY")
|
| 50 |
+
|
| 51 |
+
async def run_analysis(company_name):
|
| 52 |
+
toolset = ComposioToolSet(api_key=composio_key)
|
| 53 |
+
tools = toolset.get_tools(actions=[Action.TWITTER_RECENT_SEARCH, Action.REDDIT_SEARCH_ACROSS_SUBREDDITS, Action.HACKERNEWS_SEARCH_POSTS, Action.HACKERNEWS_GET_ITEM_WITH_ID])
|
| 54 |
+
agent = Agent(
|
| 55 |
+
name="Social Media Analyst",
|
| 56 |
+
instructions=f"""
|
| 57 |
+
Your ONLY job is to deliver a crisp, actionable, clustered executive summary of new, high-utility, real-time technical incidents, security chatter, actionable customer/developer feedback for the company, and competitive intelligence.
|
| 58 |
+
|
| 59 |
+
- Do NOT cover general market or sector sentiment, financial/market positioning, or broad sentiment.
|
| 60 |
+
- Do NOT explain what you are not doing, do NOT ask for clarification, do NOT disclaim, and do NOT ask for permission or confirmation.
|
| 61 |
+
- Just deliver the report as specified, following the structure below.
|
| 62 |
+
- You must use Reddit, Twitter, and Hacker News to get the data. For each source, find and cluster at least 3 relevant, recent signals (if available) and include them in the summary and citations.
|
| 63 |
+
- The search query for twitter has to be something like this (Adjust the parameters according to the context): "{company_name} to:{max_date} from:{min_date}", don't pass any other fields. Just the query and max results.
|
| 64 |
+
- For Twitter: If no technical incidents, bugs, outages, or actionable feedback are found, instead summarize the general sentiment about {company_name} on Twitter, with clickable links to the original tweets.
|
| 65 |
+
- For hackernews, use the search query: "{company_name}", then use the get item with id tool to get the item.
|
| 66 |
+
- Don't give updates that the company already knows about, like product launches, new features, etc.
|
| 67 |
+
- For competitive intelligence, look for posts, tweets, or comments where users compare {company_name} to other competitors. Summarize the key points, advantages, disadvantages, and user sentiment in these comparisons. Include citations for each comparison.
|
| 68 |
+
- Provide more research-detailed information in both the executive summary and competitive intelligence sections. Go deeper into technical, user, and product details, and cite specific sources and findings.
|
| 69 |
+
Report structure:
|
| 70 |
+
1. # Executive Summary: A crisp, actionable, clustered summary of all new, high-utility signals. No fluff, only what matters. Include detailed research and findings.
|
| 71 |
+
2. # Competitive Intelligence: What users are saying when comparing {company_name} to competitors. Summarize key themes, pros/cons, and user sentiment. Include citations and research-level detail.
|
| 72 |
+
3. # Exact Citations: For each cluster or item, provide clickable links to the original sources (tweets, posts, etc.).
|
| 73 |
+
|
| 74 |
+
Everything should be cited and referenced. Focus only on the tech side and actionable intelligence.
|
| 75 |
+
You will be given the name of the company.
|
| 76 |
+
If tool call fails, try again with different permutations of the query.
|
| 77 |
+
""",
|
| 78 |
+
tools=tools,
|
| 79 |
+
model='gpt-4.1',
|
| 80 |
+
)
|
| 81 |
+
res = await Runner.run(starting_agent=agent, input=f"What is the summary of {company_name}?")
|
| 82 |
+
return res.final_output
|
| 83 |
+
|
| 84 |
+
async def dashboard_result(result):
|
| 85 |
+
agent = Agent(
|
| 86 |
+
name="Dashboard Generator",
|
| 87 |
+
instructions=f"""
|
| 88 |
+
Your job is to generate a dashboard from the given result.
|
| 89 |
+
Competitors include direct competitors only.
|
| 90 |
+
""",
|
| 91 |
+
output_type=DashboardOutput,
|
| 92 |
+
)
|
| 93 |
+
res = await Runner.run(starting_agent=agent, input=result)
|
| 94 |
+
return res.final_output
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
name_from_url = st.query_params.get("name", "")
|
| 99 |
+
company_from_url = st.query_params.get("company", "")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
if name_from_url:
|
| 103 |
+
st.title(f"{name_from_url}'s Market Pulse Agent ")
|
| 104 |
+
else:
|
| 105 |
+
st.title("Market Pulse Agent")
|
| 106 |
+
|
| 107 |
+
company_name = st.text_input("Company name:", value=company_from_url)
|
| 108 |
+
|
| 109 |
+
if st.button("Analyze"):
|
| 110 |
+
if company_name:
|
| 111 |
+
with st.spinner("Analyzing social media sentiment..."):
|
| 112 |
+
result = asyncio.run(run_analysis(company_name))
|
| 113 |
+
dashboard_result_response = asyncio.run(dashboard_result(str(result)))
|
| 114 |
+
st.subheader("Summary and Sentiment Analysis")
|
| 115 |
+
st.write(result)
|
| 116 |
+
|
| 117 |
+
st.markdown("---")
|
| 118 |
+
st.header("📊 Dashboard")
|
| 119 |
+
# Metrics Row
|
| 120 |
+
metric1, metric2, metric3 = st.columns(3)
|
| 121 |
+
with metric1:
|
| 122 |
+
st.metric("Total Mentions", sum([s.count for s in dashboard_result_response.sentiment_data]))
|
| 123 |
+
with metric2:
|
| 124 |
+
st.metric("Incidents", sum([i.incidents for i in dashboard_result_response.incident_data]))
|
| 125 |
+
with metric3:
|
| 126 |
+
st.metric("Competitors Compared", len(dashboard_result_response.competitive_data)-1)
|
| 127 |
+
|
| 128 |
+
# Main Grid
|
| 129 |
+
grid1, grid2 = st.columns([2, 1])
|
| 130 |
+
with grid1:
|
| 131 |
+
st.subheader(":green[Sentiment Breakdown]")
|
| 132 |
+
df_sentiment = pd.DataFrame([s.model_dump() for s in dashboard_result_response.sentiment_data])
|
| 133 |
+
fig = px.pie(df_sentiment, names='sentiment', values='count', color='sentiment',
|
| 134 |
+
color_discrete_map={"Positive": "#22c55e", "Negative": "#ef4444", "Neutral": "#a3a3a3"})
|
| 135 |
+
fig.update_traces(textinfo='percent+label', pull=[0.05, 0.05, 0.05])
|
| 136 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 137 |
+
# Competitive Intelligence full width
|
| 138 |
+
st.subheader(":blue[Competitive Intelligence]")
|
| 139 |
+
df_comp = pd.DataFrame([c.model_dump() for c in dashboard_result_response.competitive_data])
|
| 140 |
+
fig3 = px.bar(df_comp, x='company', y='mentions', color='company', text='mentions',
|
| 141 |
+
color_discrete_sequence=px.colors.qualitative.Pastel)
|
| 142 |
+
fig3.update_layout(yaxis_title=None, xaxis_title=None, plot_bgcolor='#f8fafc')
|
| 143 |
+
st.plotly_chart(fig3, use_container_width=True)
|
| 144 |
+
with grid2:
|
| 145 |
+
if sum([i.incidents for i in dashboard_result_response.incident_data]) > 0:
|
| 146 |
+
st.subheader(":orange[Incident Frequency]")
|
| 147 |
+
df_incident = pd.DataFrame([i.model_dump() for i in dashboard_result_response.incident_data])
|
| 148 |
+
fig2 = px.bar(df_incident, x='source', y='incidents', color='source', text='incidents',
|
| 149 |
+
color_discrete_sequence=px.colors.qualitative.Set2)
|
| 150 |
+
fig2.update_layout(yaxis_title=None, xaxis_title=None, plot_bgcolor='#f8fafc')
|
| 151 |
+
st.plotly_chart(fig2, use_container_width=True)
|
| 152 |
|
| 153 |
+
st.markdown("<hr style='margin:2rem 0;' />", unsafe_allow_html=True)
|
| 154 |
+
st.subheader(":gray[Top Citations]")
|
| 155 |
+
df_cite = pd.DataFrame([c.model_dump() for c in dashboard_result_response.citations])
|
| 156 |
+
def make_clickable(url):
|
| 157 |
+
return f'<a href="{url}" target="_blank" style="color:#2563eb; text-decoration:underline;">link</a>'
|
| 158 |
+
df_cite['url'] = df_cite['url'].apply(make_clickable)
|
| 159 |
+
st.write(df_cite.to_html(escape=False, index=False, classes='tw-table tw-table-auto tw-bg-white tw-shadow-md tw-rounded-lg'), unsafe_allow_html=True)
|
| 160 |
+
else:
|
| 161 |
+
st.warning("Please enter a company name.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|