Commit f224af9a authored by MD. SHAHIDUL ISLAM's avatar MD. SHAHIDUL ISLAM

feat: add dedicated research LLM configuration, update analysis models, and…

feat: add dedicated research LLM configuration, update analysis models, and improve UI reporting structure
parent 6d978471
...@@ -38,6 +38,31 @@ _SECTION_KEYS = ( ...@@ -38,6 +38,31 @@ _SECTION_KEYS = (
"risks", "risks",
"suitability", "suitability",
) )
_SECTION_TITLES = {
"company": BilingualText(
en="What does this company do?", bn="কোম্পানিটি কী করে?"
),
"business_model": BilingualText(
en="How does it make money?", bn="এটি কীভাবে আয় করে?"
),
"profitability": BilingualText(
en="Is it actually making money?", bn="কোম্পানিটি কি সত্যিই মুনাফা করছে?"
),
"financial_safety": BilingualText(
en="Is it financially safe?", bn="এটি কি আর্থিকভাবে নিরাপদ?"
),
"valuation": BilingualText(
en="How do we judge if the price is reasonable?",
bn="দামটি যুক্তিসঙ্গত কি না আমরা কীভাবে বিচার করি?",
),
"dividends": BilingualText(
en="Does it reward shareholders?", bn="এটি কি শেয়ারহোল্ডারদের পুরস্কৃত করে?"
),
"moat": BilingualText(en="What makes it special?", bn="কী এটিকে বিশেষ করে তোলে?"),
"bull_case": BilingualText(en="Why it could do well", bn="কেন এটি ভালো করতে পারে"),
"risks": BilingualText(en="What could go wrong", bn="কী ভুল হতে পারে"),
"suitability": BilingualText(en="So, is it for you?", bn="তাহলে, এটি কি আপনার জন্য?"),
}
_FACTOR_WEIGHTS = { _FACTOR_WEIGHTS = {
"profitability": 2.5, "profitability": 2.5,
"financial_health": 2.5, "financial_health": 2.5,
...@@ -46,22 +71,27 @@ _FACTOR_WEIGHTS = { ...@@ -46,22 +71,27 @@ _FACTOR_WEIGHTS = {
"dividend": 1.5, "dividend": 1.5,
} }
_METHOD_KEYS = ("historical_pe", "peer_pe", "historical_pb", "dividend_yield") _METHOD_KEYS = ("historical_pe", "peer_pe", "historical_pb", "dividend_yield")
_EXCLUDED_RAW_EVIDENCE_KEYS = frozenset( _AI_EVIDENCE_KEYS = frozenset(
{ {
"symbol",
"company",
"analysis_date",
"market_snapshot",
"annual_financial_performance", "annual_financial_performance",
"quarterly_performance", "quarterly_performance",
"balance_sheet_history",
"shareholding_history", "shareholding_history",
"dividend_history", "dividend_history",
"nav_history", "nav_history",
"loan_status",
"operating_cash_flow_per_share_history", "operating_cash_flow_per_share_history",
"price_momentum",
"technical_evidence",
"calculated_factors",
"valuation_anchors",
} }
) )
_NARRATIVE_PATTERNS = { _NARRATIVE_PATTERNS = {
"removed dashboard UI section": re.compile(
r"\b(?:key numbers|profits?\s*&\s*dividends?|who owns it|"
r"recent DSE disclosures)\b",
re.IGNORECASE,
),
"overall /100 score": re.compile( "overall /100 score": re.compile(
r"(?:\d+(?:\.\d+)?\s*/\s*100|[০-৯]+\s*/\s*১০০)", re.IGNORECASE r"(?:\d+(?:\.\d+)?\s*/\s*100|[০-৯]+\s*/\s*১০০)", re.IGNORECASE
), ),
...@@ -126,10 +156,19 @@ class AIReportSectionOutput(BaseModel): ...@@ -126,10 +156,19 @@ class AIReportSectionOutput(BaseModel):
"risks", "risks",
"suitability", "suitability",
] ]
title: AIText
summary: AIText summary: AIText
body: list[AIText] = Field(min_length=1, max_length=4)
bullets: list[AIText] = Field(default_factory=list, max_length=6) bullets: list[AIText] = Field(default_factory=list, max_length=6)
@model_validator(mode="after")
def enforce_reference_format(self):
if self.key in {"bull_case", "risks"}:
if not 3 <= len(self.bullets) <= 6:
raise ValueError(f"{self.key} must contain 3-6 evidence bullets")
elif self.bullets:
raise ValueError("bullets are only allowed for bull_case and risks")
return self
class AITraderOutput(BaseModel): class AITraderOutput(BaseModel):
rating: Literal["Buy", "Overweight", "Hold", "Underweight", "Sell"] rating: Literal["Buy", "Overweight", "Hold", "Underweight", "Sell"]
...@@ -165,8 +204,10 @@ class AIStockResearchOutput(BaseModel): ...@@ -165,8 +204,10 @@ class AIStockResearchOutput(BaseModel):
return self return self
def _provider_kwargs(config: dict[str, Any]) -> dict[str, Any]: def _provider_kwargs(
provider = str(config.get("llm_provider", "")).lower() config: dict[str, Any], provider_override: str | None = None
) -> dict[str, Any]:
provider = str(provider_override or config.get("llm_provider", "")).lower()
kwargs: dict[str, Any] = {} kwargs: dict[str, Any] = {}
if provider == "google" and config.get("google_thinking_level"): if provider == "google" and config.get("google_thinking_level"):
kwargs["thinking_level"] = config["google_thinking_level"] kwargs["thinking_level"] = config["google_thinking_level"]
...@@ -203,17 +244,12 @@ def _agent_evidence(state: dict[str, Any] | None) -> dict[str, str]: ...@@ -203,17 +244,12 @@ def _agent_evidence(state: dict[str, Any] | None) -> dict[str, str]:
def _evidence_for_ai(evidence: dict[str, Any]) -> dict[str, Any]: def _evidence_for_ai(evidence: dict[str, Any]) -> dict[str, Any]:
"""Exclude raw datasets belonging to UI blocks that are not AI features. """Allow only the date-bounded DSE datasets needed by company analysis."""
The application may still use these DSE rows for deterministic calculations,
but the model should not receive or reproduce the removed key-number, history,
ownership, or disclosure feeds.
"""
return { return {
key: value key: value
for key, value in evidence.items() for key, value in evidence.items()
if key not in _EXCLUDED_RAW_EVIDENCE_KEYS if key in _AI_EVIDENCE_KEYS
} }
...@@ -263,14 +299,28 @@ class AIStockAnalysisGenerator: ...@@ -263,14 +299,28 @@ class AIStockAnalysisGenerator:
llm: Any | None = None, llm: Any | None = None,
) -> None: ) -> None:
self.config = config or DEFAULT_CONFIG self.config = config or DEFAULT_CONFIG
self.provider = str(self.config["llm_provider"]) self.provider = str(
self.model = str(self.config["deep_think_llm"]) self.config.get("research_llm_provider")
or self.config["llm_provider"]
)
self.model = str(
self.config.get("research_llm_model")
or self.config["deep_think_llm"]
)
if llm is None: if llm is None:
research_backend = self.config.get("research_llm_backend_url")
backend_url = (
research_backend
if research_backend not in (None, "")
else self.config.get("backend_url")
if self.provider == str(self.config["llm_provider"])
else None
)
client = create_llm_client( client = create_llm_client(
provider=self.provider, provider=self.provider,
model=self.model, model=self.model,
base_url=self.config.get("backend_url"), base_url=backend_url,
**_provider_kwargs(self.config), **_provider_kwargs(self.config, self.provider),
) )
llm = client.get_llm() llm = client.get_llm()
self.llm = llm self.llm = llm
...@@ -406,8 +456,9 @@ class AIStockAnalysisGenerator: ...@@ -406,8 +456,9 @@ class AIStockAnalysisGenerator:
sections = [ sections = [
ReportSection( ReportSection(
key=key, key=key,
title=ordered_sections[key].title.api_text(), title=_SECTION_TITLES[key],
summary=ordered_sections[key].summary.api_text(), summary=ordered_sections[key].summary.api_text(),
body=[paragraph.api_text() for paragraph in ordered_sections[key].body],
bullets=[bullet.api_text() for bullet in ordered_sections[key].bullets], bullets=[bullet.api_text() for bullet in ordered_sections[key].bullets],
) )
for key in _SECTION_KEYS for key in _SECTION_KEYS
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment