Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/agents/sage-examples.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ ______________________________________________________________________
- If cross-repo rollout is needed, keep changes here minimal and note any follow-up version bump separately.
- Do not create `venv`/`.venv`; always reuse the existing configured Python environment.

```
```
18 changes: 13 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,19 @@ jobs:
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -e ".[dev]"

- name: Install examples dependencies
run: |
pip install -e .
# This job only validates example entrypoint structure plus the direct third-party
# modules listed in examples/test_apps.py. Installing the full package pulls in the
# broader isage/isagellm graph, which causes Python 3.10 resolver backtracking.
pip install --prefer-binary \
"pytest>=7.0.0" \
"pyyaml>=6.0" \
"requests>=2.25.0" \
"pillow>=10.0.0" \
"numpy>=1.26.0,<2.3.0" \
"scikit-learn>=1.3.0" \
"opencv-python-headless>=4.5.0" \
"torch==2.7.0" \
"torchvision==0.22.0"

- name: Run example tests
run: |
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
"packageManager": "ms-python.python:conda"
}
]
}
}
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,20 @@ sage-examples/

Complete, runnable applications demonstrating real-world use cases:

| Application | Description | Script |
| ------------------------- | -------------------------- | ------------------------------------ |
| 🎬 **Video Intelligence** | Multi-model video analysis | `examples/run_video_intelligence.py` |
| 🏥 **Medical Diagnosis** | AI medical image analysis | `examples/run_medical_diagnosis.py` |
| 🏠 **Smart Home** | IoT automation system | `examples/run_smart_home.py` |
| 📰 **Article Monitoring** | News monitoring pipeline | `examples/run_article_monitoring.py` |
| 💬 **Auto-scaling Chat** | Dynamic scaling chat | `examples/run_auto_scaling_chat.py` |
| 📚 **Literature Recommendation** | Personalized scientific paper recommendation | `examples/run_literature_report_assistant.py` |
| 🧭 **Patent Landscape Mapper** | Patent clustering and whitespace opportunity mapping | `examples/run_patent_landscape_mapper.py` |
| 🎓 **Student Improvement** | Stateful learning diagnosis and score improvement MVP | `examples/run_student_improvement.py` |
| Application | Description | Script |
| -------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------- |
| 🎬 **Video Intelligence** | Multi-model video analysis | `examples/run_video_intelligence.py` |
| 🏥 **Medical Diagnosis** | AI medical image analysis | `examples/run_medical_diagnosis.py` |
| 🏠 **Smart Home** | IoT automation system | `examples/run_smart_home.py` |
| 📰 **Article Monitoring** | News monitoring pipeline | `examples/run_article_monitoring.py` |
| 💬 **Auto-scaling Chat** | Dynamic scaling chat | `examples/run_auto_scaling_chat.py` |
| 📚 **Literature Recommendation** | Personalized scientific paper recommendation | `examples/run_literature_report_assistant.py` |
| 🧭 **Patent Landscape Mapper** | Patent clustering and whitespace opportunity mapping | `examples/run_patent_landscape_mapper.py` |
| 🎓 **Student Improvement** | Stateful learning diagnosis and score improvement MVP | `examples/run_student_improvement.py` |
| 🚚 **Supply Chain Alert** | Stateful supply risk detection and dashboard snapshot MVP | `examples/run_supply_chain_alert.py` |
| 🌐 **Supply Chain Alert API** | FastAPI service for querying dashboard, alerts, and supplier risk | `examples/run_supply_chain_alert_api.py` |
| 🎫 **Ticket Triage** | Stateful customer support ticket classification, urgency scoring, and routing | `examples/run_ticket_triage.py` |
| 🌐 **Ticket Triage API** | FastAPI service for triage ingestion, queue queries, and ticket lookup | `examples/run_ticket_triage_api.py` |

See `examples/README.md` for details.

Expand Down
3 changes: 2 additions & 1 deletion apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ sage-apps/
### Core Framework

- `isage>=0.3.0` - Consolidated SAGE core (`sage.foundation`, `sage.stream`, `sage.runtime`)
- Optional adapters such as `isagellm` / `isage-rag` when a specific app needs external inference or retrieval backends
- Optional adapters such as `isagellm` / `isage-rag` when a specific app needs external inference or
retrieval backends

### Application-Specific

Expand Down
2 changes: 2 additions & 0 deletions apps/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ dependencies = [
"requests>=2.25.0", # HTTP 请求
"httpx>=0.28.0", # OpenAI-compatible client transport
"openai>=1.52.0", # OpenAI-compatible API client
"fastapi>=0.115.0,<1.0.0", # API app factory and service endpoints
"uvicorn>=0.32.0,<1.0.0", # Local ASGI server for API demos
"python-dotenv>=1.0.1", # 从 .env 加载本地模型配置
"redis>=5.0.0,<6.0.0", # Redis 缓存
"paho-mqtt>=1.6.0", # MQTT 协议
Expand Down
1 change: 1 addition & 0 deletions apps/src/sage/apps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- medical_diagnosis: AI-assisted medical imaging diagnosis
- patent_landscape_mapper: Patent clustering, whitespace analysis, and assignee mapping
- student_improvement: Personalized score improvement with stateful learning workflows
- ticket_triage: Customer support ticket triage with explicit stateful routing
- work_report_generator: Weekly/daily work report generator with GitHub integration
- literature_report_assistant: Literature search and reading report generation assistant

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,7 @@ def _select_disc_masks(
if bbox is None:
continue

x, y, w, h = [int(value) for value in bbox]
x, y, w, h = (int(value) for value in bbox)
if w <= 0 or h <= 0:
continue

Expand Down
1 change: 0 additions & 1 deletion apps/src/sage/apps/medical_diagnosis/run_diagnosis.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

from sage.apps.medical_diagnosis.agents import DiagnosticAgent


DEFAULT_CONFIG_PATH = Path(__file__).parent / "config" / "agent_config.yaml"


Expand Down
1 change: 0 additions & 1 deletion apps/src/sage/apps/medical_diagnosis/setup_data.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
DATA_DIR="$SCRIPT_DIR/data/processed"
PYTHON_BIN="${PYTHON_BIN:-python}"

Expand Down
2 changes: 1 addition & 1 deletion apps/src/sage/apps/patent_landscape_mapper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ The report includes:
- replace demo data with a CSV or API-backed patent source
- add CPC/IPC code normalization
- enrich assignee rollups with corporate-family resolution
- compare two corpora for competitor benchmarking
- compare two corpora for competitor benchmarking
2 changes: 1 addition & 1 deletion apps/src/sage/apps/patent_landscape_mapper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
"build_demo_patent_corpus",
"print_patent_landscape_report",
"run_patent_landscape_mapper_pipeline",
]
]
43 changes: 33 additions & 10 deletions apps/src/sage/apps/patent_landscape_mapper/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,12 @@ def _build_clusters(

local_matrix = matrix[patent_indexes]
similarities = cosine_similarity(local_matrix, centroid.reshape(1, -1)).reshape(-1)
representative_local_indexes = np.argsort(similarities)[::-1][: request.top_patents_per_cluster]
representative_indexes = [patent_indexes[index] for index in representative_local_indexes.tolist()]
representative_local_indexes = np.argsort(similarities)[::-1][
: request.top_patents_per_cluster
]
representative_indexes = [
patent_indexes[index] for index in representative_local_indexes.tolist()
]

assignee_breakdown = dict(Counter(record.assignee for record in cluster_patents))
top_assignee_share = max(assignee_breakdown.values()) / len(cluster_patents)
Expand Down Expand Up @@ -147,15 +151,19 @@ def _build_clusters(
return sorted(states, key=lambda item: item.whitespace_score, reverse=True)


def _build_theme_clusters(states: list[_ClusterState], patents: list[PatentRecord]) -> list[ThemeCluster]:
def _build_theme_clusters(
states: list[_ClusterState], patents: list[PatentRecord]
) -> list[ThemeCluster]:
return [
ThemeCluster(
cluster_id=state.cluster_id,
label=state.label,
top_terms=state.top_terms,
patent_ids=[patents[index].patent_id for index in state.patent_indexes],
patent_count=len(state.patent_indexes),
representative_patent_ids=[patents[index].patent_id for index in state.representative_indexes],
representative_patent_ids=[
patents[index].patent_id for index in state.representative_indexes
],
assignee_breakdown=state.assignee_breakdown,
average_focus_relevance=state.average_focus_relevance,
whitespace_score=state.whitespace_score,
Expand Down Expand Up @@ -187,7 +195,9 @@ def _build_whitespace_opportunities(
rationale=rationale,
target_keywords=state.top_terms[:3],
opportunity_score=state.whitespace_score,
supporting_patent_ids=[patents[index].patent_id for index in state.representative_indexes],
supporting_patent_ids=[
patents[index].patent_id for index in state.representative_indexes
],
)
)
return opportunities
Expand All @@ -208,7 +218,12 @@ def _build_watchlist(
for index in state.patent_indexes:
record = patents[index]
recency_score = (record.year - min_year) / year_span
watch_score = round(0.55 * float(focus_scores[index]) + 0.30 * state.whitespace_score + 0.15 * recency_score, 3)
watch_score = round(
0.55 * float(focus_scores[index])
+ 0.30 * state.whitespace_score
+ 0.15 * recency_score,
3,
)
watch_items.append(
PatentWatchItem(
patent_id=record.patent_id,
Expand All @@ -226,7 +241,9 @@ def _build_watchlist(
return sorted(watch_items, key=lambda item: item.watch_score, reverse=True)[:5]


def _build_graph(states: list[_ClusterState], patents: list[PatentRecord]) -> tuple[list[LandscapeGraphNode], list[LandscapeGraphEdge]]:
def _build_graph(
states: list[_ClusterState], patents: list[PatentRecord]
) -> tuple[list[LandscapeGraphNode], list[LandscapeGraphEdge]]:
nodes: list[LandscapeGraphNode] = []
edges: list[LandscapeGraphEdge] = []
seen_assignees: set[str] = set()
Expand Down Expand Up @@ -312,8 +329,14 @@ def analyze_patent_landscape(request: PatentLandscapeRequest) -> PatentLandscape
watchlist_patents = _build_watchlist(states, patents, focus_scores)
graph_nodes, graph_edges = _build_graph(states, patents)

top_themes = ", ".join(cluster.label for cluster in theme_clusters[: min(ceil(cluster_count / 2), 3)])
lead_opportunity = whitespace_opportunities[0].title if whitespace_opportunities else "No whitespace opportunity identified"
top_themes = ", ".join(
cluster.label for cluster in theme_clusters[: min(ceil(cluster_count / 2), 3)]
)
lead_opportunity = (
whitespace_opportunities[0].title
if whitespace_opportunities
else "No whitespace opportunity identified"
)
summary = (
f"Mapped {len(patents)} patents across {cluster_count} themes. Leading themes: {top_themes}. "
f"Top whitespace opportunity: {lead_opportunity}."
Expand All @@ -330,4 +353,4 @@ def analyze_patent_landscape(request: PatentLandscapeRequest) -> PatentLandscape
watchlist_patents=watchlist_patents,
graph_nodes=graph_nodes,
graph_edges=graph_edges,
)
)
2 changes: 1 addition & 1 deletion apps/src/sage/apps/patent_landscape_mapper/demo_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,4 @@ def build_demo_patent_corpus() -> list[PatentRecord]:
jurisdictions=["US", "EP"],
tags=["cold chain", "edge inspection", "sterile storage"],
),
]
]
2 changes: 1 addition & 1 deletion apps/src/sage/apps/patent_landscape_mapper/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,4 @@ def to_dict(self) -> dict[str, Any]:
"watchlist_patents": [item.to_dict() for item in self.watchlist_patents],
"graph_nodes": [item.to_dict() for item in self.graph_nodes],
"graph_edges": [item.to_dict() for item in self.graph_edges],
}
}
8 changes: 6 additions & 2 deletions apps/src/sage/apps/patent_landscape_mapper/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@

class ValidatePatentLandscapeRequest(MapFunction):
def execute(self, data: PatentLandscapeRequest | dict[str, Any]) -> PatentLandscapeRequest:
request = data if isinstance(data, PatentLandscapeRequest) else PatentLandscapeRequest.from_dict(data)
request = (
data
if isinstance(data, PatentLandscapeRequest)
else PatentLandscapeRequest.from_dict(data)
)
if len(request.patents) < 2:
raise ValueError("Patent landscape mapper requires at least two patents.")
if request.cluster_count < 2:
Expand Down Expand Up @@ -170,4 +174,4 @@ def main() -> None:


if __name__ == "__main__":
main()
main()
2 changes: 1 addition & 1 deletion apps/src/sage/apps/student_improvement/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,4 @@ export SAGE_OPENAI_MODEL="<optional-model-id>"

- 将错因分析和解法生成替换为 LLM 增强版本
- 把本地状态仓替换为真正的 shared state / service backend
- 增加更丰富的题型解析、标签体系和长期学习轨迹建模
- 增加更丰富的题型解析、标签体系和长期学习轨迹建模
32 changes: 24 additions & 8 deletions apps/src/sage/apps/student_improvement/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ def _format_knowledge_names(knowledge_point_ids: list[str], graph_payload: dict[
return "、".join(names)


def _print_exam_snapshot(title: str, payload: dict[str, Any], graph_payload: dict[str, Any]) -> None:
def _print_exam_snapshot(
title: str, payload: dict[str, Any], graph_payload: dict[str, Any]
) -> None:
_print_section(title)
print(
f"成绩: {payload['total_score']}/{payload['max_score']} "
Expand All @@ -87,7 +89,9 @@ def _print_exam_snapshot(title: str, payload: dict[str, Any], graph_payload: dic
print(f"新增错题数: {payload['wrong_question_count']}")
print(
"薄弱知识点: "
+ "、".join(item["knowledge_point"] for item in payload["diagnosis"]["weak_knowledge_points"][:3])
+ "、".join(
item["knowledge_point"] for item in payload["diagnosis"]["weak_knowledge_points"][:3]
)
)
current_exam_time = payload["diagnosis"]["generated_at"]
new_wrong_questions = [
Expand Down Expand Up @@ -144,7 +148,9 @@ def _print_mastery_changes(
)


def _print_wrong_bank(final_wrong_bank: list[dict[str, Any]], graph_payload: dict[str, Any]) -> None:
def _print_wrong_bank(
final_wrong_bank: list[dict[str, Any]], graph_payload: dict[str, Any]
) -> None:
_print_section("动态错题集")
for item in final_wrong_bank:
knowledge_names = _format_knowledge_names(item["knowledge_point_ids"], graph_payload)
Expand Down Expand Up @@ -225,7 +231,9 @@ def run_demo_once(storage_path: str | Path | None = None, *, json_mode: bool = F

print("\n📘 SAGE Personalized Score Improvement MVP")
print("展示流程: 初始化课程 -> 导入第一次考试 -> 导入第二次考试 -> 查看状态更新\n")
print(f"学生: {final_profile['student_id']} | {final_profile['grade']} {final_profile['subject']}")
print(
f"学生: {final_profile['student_id']} | {final_profile['grade']} {final_profile['subject']}"
)
print("课程: 八年级数学教材知识图谱 + 两次考试增量导入")
print(f"诊断概览: {final_profile['diagnosis_overview']}")

Expand Down Expand Up @@ -305,7 +313,9 @@ def _next_demo_exam(self):
return exam
return None

def import_next_demo_exam(self, *, render: bool = False) -> tuple[dict[str, Any], dict[str, Any]] | None:
def import_next_demo_exam(
self, *, render: bool = False
) -> tuple[dict[str, Any], dict[str, Any]] | None:
next_exam = self._next_demo_exam()
if next_exam is None:
print("所有 demo 考试都已经导入完毕。可选择查看总览,或重置 demo 状态。")
Expand All @@ -324,7 +334,9 @@ def import_next_demo_exam(self, *, render: bool = False) -> tuple[dict[str, Any]

def show_dashboard(self) -> None:
try:
profile = _to_payload(_quiet_runtime_call(self.service.get_student_profile, self.student_id))
profile = _to_payload(
_quiet_runtime_call(self.service.get_student_profile, self.student_id)
)
diagnosis = _to_payload(
_quiet_runtime_call(self.service.get_student_diagnosis, self.student_id)
)
Expand Down Expand Up @@ -372,7 +384,9 @@ def show_wrong_bank(self) -> None:
_print_wrong_bank(wrong_bank, graph)

def show_knowledge_graph(self) -> None:
graph = _to_payload(_quiet_runtime_call(self.service.get_student_knowledge_graph, self.student_id))
graph = _to_payload(
_quiet_runtime_call(self.service.get_student_knowledge_graph, self.student_id)
)
_print_knowledge_graph(graph)

def test_llm_connection(self) -> None:
Expand All @@ -394,7 +408,9 @@ def test_llm_connection(self) -> None:

def show_ai_guidance(self) -> None:
try:
profile = _to_payload(_quiet_runtime_call(self.service.get_student_profile, self.student_id))
profile = _to_payload(
_quiet_runtime_call(self.service.get_student_profile, self.student_id)
)
diagnosis = _to_payload(
_quiet_runtime_call(self.service.get_student_diagnosis, self.student_id)
)
Expand Down
Loading
Loading