Ig0tU Claude Sonnet 4.6 commited on
Commit
a3efffb
·
1 Parent(s): f6a2352

Fix RSS sync: switch to feedparser, add feed_title to output

Browse files

xml.etree.ElementTree was failing silently on CDATA links and encoding
declarations. feedparser handles all RSS/Atom variants correctly.

Also: show feed title in dashboard output, better empty-feed messaging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (2) hide show
  1. requirements.txt +1 -0
  2. signalmesh_api.py +28 -23
requirements.txt CHANGED
@@ -2,3 +2,4 @@ fastapi==0.115.6
2
  uvicorn[standard]==0.32.1
3
  pydantic==2.10.3
4
  httpx==0.28.1
 
 
2
  uvicorn[standard]==0.32.1
3
  pydantic==2.10.3
4
  httpx==0.28.1
5
+ feedparser==6.0.11
signalmesh_api.py CHANGED
@@ -45,8 +45,8 @@ _app_pkg.logger = _logger_pkg.logger
45
 
46
  import asyncio
47
  import re
48
- import xml.etree.ElementTree as ET
49
 
 
50
  import httpx
51
  from fastapi import FastAPI, Header, HTTPException, Request
52
  from fastapi.middleware.cors import CORSMiddleware
@@ -446,9 +446,13 @@ async function doRssSync() {
446
  appendFeed('rss-feed', `<span class="tag err">ERR</span>${escHtml(String(d.error||d.detail))}`);
447
  return;
448
  }
449
- appendFeed('rss-feed', `<span class="tag rss">RSS</span><b>${d.synced}</b> items synced ${d.signals_in_mesh} signals in mesh`);
450
- (d.items||[]).slice(0,8).forEach(it =>
451
- appendFeed('rss-feed', `<span class="dim">↳ node <b>${it.node}</b> (${it.agent})</span> ${escHtml(it.title?.slice(0,60)||'')}`)
 
 
 
 
452
  );
453
  refreshMesh();
454
  }
@@ -465,7 +469,7 @@ async function doContextDiscovery() {
465
  }
466
  appendFeed('ctx-feed', `<span class="tag live">FOUND</span><b>${d.urls_found}</b> URLs · <b>${d.signals_broadcast}</b> broadcast · <b>${(d.rss_feeds_synced||[]).length}</b> RSS feeds`);
467
  (d.rss_feeds_synced||[]).forEach(r =>
468
- appendFeed('ctx-feed', `<span class="tag rss">RSS</span>${escHtml(r.url?.slice(0,50))} → ${r.synced ?? r.error} ${r.error?'<span class="tag err">ERR</span>':'items'}`)
469
  );
470
  refreshMesh();
471
  }
@@ -695,32 +699,32 @@ async def tool_rss_sync(
695
  ):
696
  """
697
  **Tool: rss_matrix_sync**
698
- Fetch an RSS feed and sync each item into the spatial grid via SHA-256 URI routing.
699
- Each feed item is broadcast onto its deterministically assigned grid node.
700
  """
701
  _check_key(x_signalmesh_key)
 
 
702
  try:
703
  async with httpx.AsyncClient(timeout=30.0) as client:
704
- resp = await client.get(req.url, follow_redirects=True)
 
 
 
 
705
  resp.raise_for_status()
 
706
  except Exception as e:
707
- raise HTTPException(status_code=502, detail=f"Failed to fetch RSS feed: {e}")
708
 
709
- try:
710
- root_el = ET.fromstring(resp.content)
711
- except ET.ParseError as e:
712
- raise HTTPException(status_code=422, detail=f"Invalid XML in feed: {e}")
713
 
714
- items = root_el.findall(".//item") or root_el.findall(".//{http://www.w3.org/2005/Atom}entry")
715
  synced = []
716
- for item in items:
717
- title_el = item.find("title") or item.find("{http://www.w3.org/2005/Atom}title")
718
- link_el = item.find("link") or item.find("{http://www.w3.org/2005/Atom}link")
719
- desc_el = item.find("description") or item.find("{http://www.w3.org/2005/Atom}summary")
720
-
721
- title = title_el.text if title_el is not None else "Untitled"
722
- link = (link_el.text or link_el.get("href", "")) if link_el is not None else ""
723
- desc = (desc_el.text or "")[:400] if desc_el is not None else ""
724
 
725
  if not link:
726
  continue
@@ -732,7 +736,7 @@ async def tool_rss_sync(
732
  signal_registry.broadcast(
733
  name=f"rss_{agent_kw}",
734
  source_type="rss_cp",
735
- data={"title": title, "link": link, "summary": desc},
736
  metadata={"node": node_idx, "agent": agent_kw, "feed_url": req.url},
737
  )
738
  synced.append({"title": title, "node": node_idx, "agent": agent_kw, "link": link})
@@ -740,6 +744,7 @@ async def tool_rss_sync(
740
  return {
741
  "synced": len(synced),
742
  "feed_url": req.url,
 
743
  "items": synced[:20],
744
  "signals_in_mesh": _signals_in_mesh(),
745
  }
 
45
 
46
  import asyncio
47
  import re
 
48
 
49
+ import feedparser
50
  import httpx
51
  from fastapi import FastAPI, Header, HTTPException, Request
52
  from fastapi.middleware.cors import CORSMiddleware
 
446
  appendFeed('rss-feed', `<span class="tag err">ERR</span>${escHtml(String(d.error||d.detail))}`);
447
  return;
448
  }
449
+ const feedLabel = d.feed_title ? `<b style="color:var(--accent2)">${escHtml(d.feed_title)}</b> · ` : '';
450
+ appendFeed('rss-feed', `<span class="tag rss">RSS</span>${feedLabel}<b>${d.synced}</b> items → ${d.signals_in_mesh} signals in mesh`);
451
+ if (d.synced === 0) {
452
+ appendFeed('rss-feed', `<span class="dim">Feed returned no items. Try a different URL.</span>`);
453
+ }
454
+ (d.items||[]).slice(0,10).forEach(it =>
455
+ appendFeed('rss-feed', `<span class="dim">↳ node <b>${it.node}</b> (${it.agent})</span> ${escHtml(it.title?.slice(0,70)||'')}`)
456
  );
457
  refreshMesh();
458
  }
 
469
  }
470
  appendFeed('ctx-feed', `<span class="tag live">FOUND</span><b>${d.urls_found}</b> URLs · <b>${d.signals_broadcast}</b> broadcast · <b>${(d.rss_feeds_synced||[]).length}</b> RSS feeds`);
471
  (d.rss_feeds_synced||[]).forEach(r =>
472
+ appendFeed('ctx-feed', `<span class="tag rss">RSS</span>${escHtml(r.url?.slice(0,50))} → ${r.error ? '<span class="tag err">ERR</span> '+escHtml(r.error) : '<b>'+r.synced+'</b> items'}`)
473
  );
474
  refreshMesh();
475
  }
 
699
  ):
700
  """
701
  **Tool: rss_matrix_sync**
702
+ Fetch an RSS/Atom feed and sync each item into the spatial grid via SHA-256 URI routing.
 
703
  """
704
  _check_key(x_signalmesh_key)
705
+
706
+ # feedparser handles RSS 0.9x/1.0/2.0, Atom, encoding, CDATA — all of it
707
  try:
708
  async with httpx.AsyncClient(timeout=30.0) as client:
709
+ resp = await client.get(
710
+ req.url,
711
+ follow_redirects=True,
712
+ headers={"User-Agent": "ToolTuner-SignalMesh/1.0 (RSS reader; +https://huggingface.co/spaces/acecalisto3/ToolTuner)"},
713
+ )
714
  resp.raise_for_status()
715
+ raw = resp.text
716
  except Exception as e:
717
+ raise HTTPException(status_code=502, detail=f"Fetch failed: {e}")
718
 
719
+ feed = feedparser.parse(raw)
720
+ if feed.bozo and not feed.entries:
721
+ raise HTTPException(status_code=422, detail=f"Feed parse error: {feed.bozo_exception}")
 
722
 
 
723
  synced = []
724
+ for entry in feed.entries:
725
+ title = entry.get("title", "Untitled")
726
+ link = entry.get("link", "") or entry.get("id", "")
727
+ summary = entry.get("summary", entry.get("description", ""))[:400]
 
 
 
 
728
 
729
  if not link:
730
  continue
 
736
  signal_registry.broadcast(
737
  name=f"rss_{agent_kw}",
738
  source_type="rss_cp",
739
+ data={"title": title, "link": link, "summary": summary},
740
  metadata={"node": node_idx, "agent": agent_kw, "feed_url": req.url},
741
  )
742
  synced.append({"title": title, "node": node_idx, "agent": agent_kw, "link": link})
 
744
  return {
745
  "synced": len(synced),
746
  "feed_url": req.url,
747
+ "feed_title": feed.feed.get("title", ""),
748
  "items": synced[:20],
749
  "signals_in_mesh": _signals_in_mesh(),
750
  }